How to add Custom Values to App Insights Logging – Monitor-OpenTelemetry

With Microsoft recommending that any new projects migrate from the old appinsights Node.JS module to @azure/monitor-opentelemetry I figured I’d bite the bullet and upgrade one of my own projects and share the results.

I’ll also cover how to add custom values to your logs (referred to as customDimensions from here).

Migration Steps

The migration process was relatively simple, I removed the old import for the applicationinsights module and added two new imports in it’s place as per below –

Imports

Old Import Code
let appInsights = require('applicationinsights');
New Import Code
const { useAzureMonitor } = require("@azure/monitor-opentelemetry");
const { trace } = require('@opentelemetry/api');

Setup Code

I then removed the code that set up the appInsights client, simplifying it as per below –

Old Setup Code
var client;
appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING)
	.setSendLiveMetrics(true)
	.start()
	
client = appInsights.defaultClient;
New Setup Code
useAzureMonitor();

Custom TelemetryProcessor

The majority of the required changes involved modernising the old Telemetry Processor, which was responsible for adding custom properties to Application Insights logs.

Previously, it handled data such as a unique device identifier, language preferences, browser details, and more. The original implementation used middleware to extract various useful pieces of information from the Express request and then passed this data to the Telemetry Processor, which added it to the outgoing logs.

The new approach is significantly more streamlined. Instead of relying on the Telemetry Processor, you now define middleware that directly accesses the currently active span, essentially the log being generated for the current request.

Attributes can then be added directly to this span, simplifying the process and reducing complexity.

The Old Code
// Define a custom Telemetry Processor to add additional data to each request log
client.addTelemetryProcessor((envelope, context) => {
	if (envelope.data.baseType === 'RequestData') {
		const customProperties = envelope?.data?.baseData?.properties ?? {};

		if (customProperties == null) {
			const browserName = customProperties.browserName || 'Unknown';
			const browserVersion = customProperties.browserVersion || 'Unknown';
			const browserEngine = customProperties.browserEngine || 'Unknown';
			const browserEngineVersion = customProperties.browserEngineVersion || 'Unknown';
			const language = customProperties.language || 'Unknown';
			const deviceId = customProperties.deviceId || 'Unknown';

			envelope.data.baseData.properties = {
				...envelope.data.baseData.properties,
				browserName: browserName,
				browserVersion: browserVersion,
				browserEngine: browserEngine,
				browserEngineVersion: browserEngineVersion,
				language: language,
				deviceId: deviceId,
			};
		}
	}

	return true;
});

// Middleware that grabs information from each request and populates the default app insights client with additional data
app.use((req, res, next) => {
	const uaParser = new UAParser();

	// Add user-specific tags
	const userId = req?.oidc?.user?.sub ?? 'anonymous';
	client.context.tags[appInsights.defaultClient.context.keys.userId] = userId;
	
	var ipAddresses = null;
	if(userId != 'anonymous') {
		const userAgent = req.headers['user-agent']; // Extract User-Agent header
		const clientInfo = uaParser.setUA(userAgent).getResult();

		// Extract IP info
		ipAddresses = req.header('x-forwarded-for')?.split(':', 1)[0] || req.socket.remoteAddress;
		client.context.tags[appInsights.defaultClient.context.keys.locationIp] = ipAddresses || '0.0.0.0';
		
		// Extract OS info
		const osName = clientInfo.os.name || 'Unknown'
		const osVersion = clientInfo.os.version || 'Unknown'
		client.context.tags[appInsights.defaultClient.context.keys.deviceOSVersion] = `${osName} ${osVersion}`
		
		// Check for an existing Device ID in the cookie
		let deviceId = req.cookies.deviceId;
		if (!deviceId) {
			// Generate a new unique Device ID
			deviceId = crypto.randomUUID(); // Generates a random UUID
			res.cookie('deviceId', deviceId, { httpOnly: true, secure: true }); // Store in cookie
		}
		
		// Extract browser information
		appInsights.defaultClient.commonProperties['browserName'] = clientInfo.browser.name || 'Unknown'; // e.g., 'Chrome', 'Firefox'
		appInsights.defaultClient.commonProperties['browserVersion'] = clientInfo.browser.version || 'Unknown'; // e.g., '96.0.4664.45'
		appInsights.defaultClient.commonProperties['browserEngine'] = clientInfo.engine.name || 'Unknown'
		appInsights.defaultClient.commonProperties['browserEngineVersion'] = clientInfo.engine.version || 'Unknown'
		appInsights.defaultClient.commonProperties['language'] = req.headers['accept-language'] || 'Unknown' // e.g., en-US, fr-FR
		appInsights.defaultClient.commonProperties['deviceId'] = deviceId || 'Unknown' // e.g., en-US, fr-FR
	}

	next();
});
The New Middleware Code
// Linking user details with Log Analytics
app.use((req, res, next) => {
	// Get the currently active span to attach data to
	const currentSpan = trace.getActiveSpan()

	if(currentSpan) {
		const uaParser = new UAParser();
		
		// Track the users unique ID
		currentSpan.setAttribute('user.id', req?.oidc?.user?.sub ?? 'anonymous')
		
		// If it's a logged in user then track additional info
		if(req?.oidc?.user?.sub) {
			const userAgent = req.headers['user-agent']; // Extract User-Agent header
			const clientInfo = uaParser.setUA(userAgent).getResult();

			// Extract IP info
			var ipAddresses = req.header('x-forwarded-for')?.split(':', 1)[0] || req.socket.remoteAddress;
			currentSpan.setAttribute('http.client_ip', ipAddresses || '0.0.0.0')
			
			// Extract OS info
			const osName = clientInfo.os.name || 'Unknown'
			const osVersion = clientInfo.os.version || 'Unknown'
			
			// Check for an existing Device ID in the cookie
			let deviceId = req.cookies.deviceId;
			if (!deviceId) {
				// Generate a new unique Device ID
				deviceId = crypto.randomUUID(); // Generates a random UUID
				res.cookie('deviceId', deviceId, { httpOnly: true, secure: true }); // Store in cookie
			}
			
			// Add all required attributes to the active span
			currentSpan.setAttribute('osName', `${osName} ${osVersion}`);
			currentSpan.setAttribute('browserName',  clientInfo?.browser?.name ?? 'Unknown'); // e.g., 'Chrome', 'Firefox'
			currentSpan.setAttribute('browserVersion',  clientInfo?.browser?.version ?? 'Unknown'); // e.g., '96.0.4664.45'
			currentSpan.setAttribute('browserEngine',  clientInfo?.engine?.name ?? 'Unknown')
			currentSpan.setAttribute('browserEngineVersion',  clientInfo.engine.version ?? 'Unknown')
			currentSpan.setAttribute('language',  req.headers?.['accept-language'] ??'Unknown') // e.g., en-US, fr-FR
			currentSpan.setAttribute('deviceId', deviceId || 'Unknown')
		}
	}

	next();
});

My Thoughts

The new module seems like a step up, dramatically simplifying the way that we handle custom attributes.

The only slight pain point I’ve identified is that App Insights looks rather bare when querying it as the new open standard doesn’t seem to fill in many of the built-in fields such as user_Id and application_Version. This could just be me missing how to handle that though.

Also I’ll be upfront in that this was a very minor application in the grand scheme of things and you’ll likely encounter additional issues that I’ll have entirely missed so please do share anything below.

Additional information around how to migrate can be found at Migrate from the Node.js Application Insights SDK 2.X to Azure Monitor OpenTelemetry

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Design a site like this with WordPress.com
Get started