diff --git a/packages/dd-trace/src/azure_metadata.js b/packages/dd-trace/src/azure_metadata.js new file mode 100644 index 0000000000..94c29c9dd1 --- /dev/null +++ b/packages/dd-trace/src/azure_metadata.js @@ -0,0 +1,120 @@ +'use strict' + +// eslint-disable-next-line max-len +// Modeled after https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/ddcommon/src/azure_app_services.rs + +const os = require('os') +const { getIsAzureFunction } = require('./serverless') + +function extractSubscriptionID (ownerName) { + if (ownerName !== undefined) { + const subId = ownerName.split('+')[0].trim() + if (subId.length > 0) { + return subId + } + } + return undefined +} + +function extractResourceGroup (ownerName) { + return /.+\+(.+)-.+webspace(-Linux)?/.exec(ownerName)?.[1] +} + +function buildResourceID (subscriptionID, siteName, resourceGroup) { + if (subscriptionID === undefined || siteName === undefined || resourceGroup === undefined) { + return undefined + } + return `/subscriptions/${subscriptionID}/resourcegroups/${resourceGroup}/providers/microsoft.web/sites/${siteName}` + .toLowerCase() +} + +function trimObject (obj) { + Object.entries(obj) + .filter(([_, value]) => value === undefined) + .forEach(([key, _]) => { delete obj[key] }) + return obj +} + +function buildMetadata () { + const { + COMPUTERNAME, + DD_AAS_DOTNET_EXTENSION_VERSION, + FUNCTIONS_EXTENSION_VERSION, + FUNCTIONS_WORKER_RUNTIME, + FUNCTIONS_WORKER_RUNTIME_VERSION, + WEBSITE_INSTANCE_ID, + WEBSITE_OWNER_NAME, + WEBSITE_OS, + WEBSITE_RESOURCE_GROUP, + WEBSITE_SITE_NAME + } = process.env + + const subscriptionID = extractSubscriptionID(WEBSITE_OWNER_NAME) + + const siteName = WEBSITE_SITE_NAME + + const [siteKind, siteType] = getIsAzureFunction() + ? ['functionapp', 'function'] + : ['app', 'app'] + + const resourceGroup = WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME) + + return trimObject({ + extensionVersion: DD_AAS_DOTNET_EXTENSION_VERSION, + functionRuntimeVersion: FUNCTIONS_EXTENSION_VERSION, + instanceID: WEBSITE_INSTANCE_ID, + instanceName: COMPUTERNAME, + operatingSystem: WEBSITE_OS ?? os.platform(), + resourceGroup, + resourceID: buildResourceID(subscriptionID, siteName, resourceGroup), + runtime: FUNCTIONS_WORKER_RUNTIME, + runtimeVersion: FUNCTIONS_WORKER_RUNTIME_VERSION, + siteKind, + siteName, + siteType, + subscriptionID + }) +} + +function getAzureAppMetadata () { + // DD_AZURE_APP_SERVICES is an environment variable introduced by the .NET APM team and is set automatically for + // anyone using the Datadog APM Extensions (.NET, Java, or Node) for Windows Azure App Services + // eslint-disable-next-line max-len + // See: https://github.com/DataDog/datadog-aas-extension/blob/01f94b5c28b7fa7a9ab264ca28bd4e03be603900/node/src/applicationHost.xdt#L20-L21 + return process.env.DD_AZURE_APP_SERVICES !== undefined ? buildMetadata() : undefined +} + +function getAzureFunctionMetadata () { + return getIsAzureFunction() ? buildMetadata() : undefined +} + +// eslint-disable-next-line max-len +// Modeled after https://github.com/DataDog/libdatadog/blob/92272e90a7919f07178f3246ef8f82295513cfed/profiling/src/exporter/mod.rs#L187 +// eslint-disable-next-line max-len +// and https://github.com/DataDog/libdatadog/blob/f3994857a59bb5679a65967138c5a3aec418a65f/trace-utils/src/trace_utils.rs#L533 +function getAzureTagsFromMetadata (metadata) { + if (metadata === undefined) { + return {} + } + return trimObject({ + 'aas.environment.extension_version': metadata.extensionVersion, + 'aas.environment.function_runtime': metadata.functionRuntimeVersion, + 'aas.environment.instance_id': metadata.instanceID, + 'aas.environment.instance_name': metadata.instanceName, + 'aas.environment.os': metadata.operatingSystem, + 'aas.environment.runtime': metadata.runtime, + 'aas.environment.runtime_version': metadata.runtimeVersion, + 'aas.resource.group': metadata.resourceGroup, + 'aas.resource.id': metadata.resourceID, + 'aas.site.kind': metadata.siteKind, + 'aas.site.name': metadata.siteName, + 'aas.site.type': metadata.siteType, + 'aas.subscription.id': metadata.subscriptionID + }) +} + +module.exports = { + getAzureAppMetadata, + getAzureFunctionMetadata, + getAzureTagsFromMetadata +} diff --git a/packages/dd-trace/src/profiling/config.js b/packages/dd-trace/src/profiling/config.js index 538400aaa7..3c360d65f7 100644 --- a/packages/dd-trace/src/profiling/config.js +++ b/packages/dd-trace/src/profiling/config.js @@ -14,6 +14,7 @@ const { oomExportStrategies, snapshotKinds } = require('./constants') const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('../plugins/util/tags') const { tagger } = require('./tagger') const { isFalse, isTrue } = require('../util') +const { getAzureTagsFromMetadata, getAzureAppMetadata } = require('../azure_metadata') class Config { constructor (options = {}) { @@ -71,7 +72,8 @@ class Config { this.tags = Object.assign( tagger.parse(DD_TAGS), tagger.parse(options.tags), - tagger.parse({ env, host, service, version, functionname }) + tagger.parse({ env, host, service, version, functionname }), + getAzureTagsFromMetadata(getAzureAppMetadata()) ) // Add source code integration tags if available diff --git a/packages/dd-trace/test/azure_metadata.spec.js b/packages/dd-trace/test/azure_metadata.spec.js new file mode 100644 index 0000000000..7a8cb787d7 --- /dev/null +++ b/packages/dd-trace/test/azure_metadata.spec.js @@ -0,0 +1,109 @@ +'use strict' + +require('./setup/tap') + +const os = require('os') +const { getAzureAppMetadata, getAzureTagsFromMetadata } = require('../src/azure_metadata') + +describe('Azure metadata', () => { + describe('for apps is', () => { + it('not provided without DD_AZURE_APP_SERVICES', () => { + delete process.env.DD_AZURE_APP_SERVICES + expect(getAzureAppMetadata()).to.be.undefined + }) + + it('provided with DD_AZURE_APP_SERVICES', () => { + delete process.env.COMPUTERNAME // actually defined on Windows + process.env.DD_AZURE_APP_SERVICES = '1' + delete process.env.WEBSITE_SITE_NAME + expect(getAzureAppMetadata()).to.deep.equal({ operatingSystem: os.platform(), siteKind: 'app', siteType: 'app' }) + }) + }) + + it('provided completely with minimum vars', () => { + delete process.env.WEBSITE_RESOURCE_GROUP + delete process.env.WEBSITE_OS + delete process.env.FUNCTIONS_EXTENSION_VERSION + delete process.env.FUNCTIONS_WORKER_RUNTIME + delete process.env.FUNCTIONS_WORKER_RUNTIME_VERSION + process.env.COMPUTERNAME = 'boaty_mcboatface' + process.env.DD_AZURE_APP_SERVICES = '1' + process.env.WEBSITE_SITE_NAME = 'website_name' + process.env.WEBSITE_OWNER_NAME = 'subscription_id+resource_group-regionwebspace' + process.env.WEBSITE_INSTANCE_ID = 'instance_id' + process.env.DD_AAS_DOTNET_EXTENSION_VERSION = '1.0' + const expected = { + extensionVersion: '1.0', + instanceID: 'instance_id', + instanceName: 'boaty_mcboatface', + operatingSystem: os.platform(), + resourceGroup: 'resource_group', + resourceID: + '/subscriptions/subscription_id/resourcegroups/resource_group/providers/microsoft.web/sites/website_name', + siteKind: 'app', + siteName: 'website_name', + siteType: 'app', + subscriptionID: 'subscription_id' + } + expect(getAzureAppMetadata()).to.deep.equal(expected) + }) + + it('provided completely with complete vars', () => { + process.env.COMPUTERNAME = 'boaty_mcboatface' + process.env.DD_AZURE_APP_SERVICES = '1' + process.env.WEBSITE_SITE_NAME = 'website_name' + process.env.WEBSITE_RESOURCE_GROUP = 'resource_group' + process.env.WEBSITE_OWNER_NAME = 'subscription_id+foo-regionwebspace' + process.env.WEBSITE_OS = 'windows' + process.env.WEBSITE_INSTANCE_ID = 'instance_id' + process.env.FUNCTIONS_EXTENSION_VERSION = '20' + process.env.FUNCTIONS_WORKER_RUNTIME = 'node' + process.env.FUNCTIONS_WORKER_RUNTIME_VERSION = '14' + process.env.DD_AAS_DOTNET_EXTENSION_VERSION = '1.0' + const expected = { + extensionVersion: '1.0', + functionRuntimeVersion: '20', + instanceID: 'instance_id', + instanceName: 'boaty_mcboatface', + operatingSystem: 'windows', + resourceGroup: 'resource_group', + resourceID: + '/subscriptions/subscription_id/resourcegroups/resource_group/providers/microsoft.web/sites/website_name', + runtime: 'node', + runtimeVersion: '14', + siteKind: 'functionapp', + siteName: 'website_name', + siteType: 'function', + subscriptionID: 'subscription_id' + } + expect(getAzureAppMetadata()).to.deep.equal(expected) + }) + + it('tags are correctly generated from vars', () => { + delete process.env.WEBSITE_RESOURCE_GROUP + delete process.env.WEBSITE_OS + delete process.env.FUNCTIONS_EXTENSION_VERSION + delete process.env.FUNCTIONS_WORKER_RUNTIME + delete process.env.FUNCTIONS_WORKER_RUNTIME_VERSION + process.env.COMPUTERNAME = 'boaty_mcboatface' + process.env.DD_AZURE_APP_SERVICES = '1' + process.env.WEBSITE_SITE_NAME = 'website_name' + process.env.WEBSITE_OWNER_NAME = 'subscription_id+resource_group-regionwebspace' + process.env.WEBSITE_INSTANCE_ID = 'instance_id' + process.env.DD_AAS_DOTNET_EXTENSION_VERSION = '1.0' + const expected = { + 'aas.environment.extension_version': '1.0', + 'aas.environment.instance_id': 'instance_id', + 'aas.environment.instance_name': 'boaty_mcboatface', + 'aas.environment.os': os.platform(), + 'aas.resource.group': 'resource_group', + 'aas.resource.id': + '/subscriptions/subscription_id/resourcegroups/resource_group/providers/microsoft.web/sites/website_name', + 'aas.site.kind': 'app', + 'aas.site.name': 'website_name', + 'aas.site.type': 'app', + 'aas.subscription.id': 'subscription_id' + } + expect(getAzureTagsFromMetadata(getAzureAppMetadata())).to.deep.equal(expected) + }) +})