Skip to content

Commit 44990b4

Browse files
authored
feat(resource-detector-azure): Add AKS Resource Detector (open-telemetry#3371)
1 parent 8c20cf0 commit 44990b4

File tree

7 files changed

+318
-0
lines changed

7 files changed

+318
-0
lines changed

packages/resource-detector-azure/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,43 @@ This package implements Semantic Convention [Version 1.37.0](https://github.com/
7171
| cloud.resource_id | The Azure Resource Manager URI uniquely identifying the Azure Virtual Machine. It typically follows this format: /subscriptions/{subscriptionId}/resourceGroups/{groupName}/providers/Microsoft.Compute/virtualMachines/{vmName}. Value from resourceId key on /metadata/instance/compute request. |
7272
| process.pid | The process ID collected from the running process. |
7373

74+
### Azure Kubernetes Service (AKS) Resource Detector
75+
76+
The AKS detector reads cluster metadata from the native `aks-cluster-metadata` ConfigMap in the `kube-public` namespace, which is automatically created by AKS. The ConfigMap contains a `clusterResourceId` key with the full ARM resource ID of the cluster.
77+
78+
| Resource Attribute | Description |
79+
|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
80+
| cloud.platform | The cloud platform. Here, it's always "azure.aks". |
81+
| cloud.provider | The cloud service provider. In this context, it's always "azure". |
82+
| cloud.resource_id | The Azure Resource Manager URI uniquely identifying the AKS cluster. Value from `clusterResourceId` key in the `aks-cluster-metadata` ConfigMap. |
83+
| k8s.cluster.name | The name of the AKS cluster, extracted from the ARM resource ID. |
84+
85+
#### Usage
86+
87+
To use the AKS detector, configure your pod to read from the native `aks-cluster-metadata` ConfigMap in the `kube-public` namespace:
88+
89+
```yaml
90+
env:
91+
- name: CLUSTER_RESOURCE_ID
92+
valueFrom:
93+
configMapKeyRef:
94+
name: aks-cluster-metadata
95+
key: clusterResourceId
96+
```
97+
98+
Alternatively, you can mount the ConfigMap as a file at `/etc/kubernetes/aks-cluster-metadata`.
99+
100+
```typescript
101+
import { detectResources } from '@opentelemetry/resources';
102+
import { azureAksDetector } from '@opentelemetry/resource-detector-azure';
103+
104+
const resource = detectResources({
105+
detectors: [azureAksDetector],
106+
});
107+
108+
const tracerProvider = new NodeTracerProvider({ resource });
109+
```
110+
74111
## Useful links
75112

76113
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as fs from 'fs';
18+
19+
import { diag } from '@opentelemetry/api';
20+
import { ResourceDetector, DetectedResource } from '@opentelemetry/resources';
21+
import {
22+
ATTR_CLOUD_PLATFORM,
23+
ATTR_CLOUD_PROVIDER,
24+
ATTR_K8S_CLUSTER_NAME,
25+
CLOUD_PLATFORM_VALUE_AZURE_AKS,
26+
CLOUD_PROVIDER_VALUE_AZURE,
27+
} from '../semconv';
28+
import {
29+
AKS_CLUSTER_RESOURCE_ID,
30+
AKS_METADATA_FILE_PATH,
31+
AksClusterMetadata,
32+
CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE,
33+
extractClusterNameFromResourceId,
34+
} from '../types';
35+
36+
/**
37+
* The AzureAksDetector can be used to detect if a process is running in an Azure Kubernetes Service (AKS) cluster.
38+
* It reads cluster metadata from environment variables populated from the aks-cluster-metadata ConfigMap
39+
* in the kube-public namespace, or from the ConfigMap file if mounted.
40+
*
41+
* The ConfigMap contains a single key 'clusterResourceId' with the full ARM resource ID.
42+
* The cluster name is extracted from this resource ID.
43+
*
44+
* @returns a {@link Resource} populated with data about the AKS environment or an empty Resource if detection fails.
45+
*/
46+
class AzureAksDetector implements ResourceDetector {
47+
public detect(): DetectedResource {
48+
let attributes = {};
49+
50+
const metadata = this.getAksMetadata();
51+
if (metadata && (metadata.name || metadata.resourceId)) {
52+
attributes = {
53+
[ATTR_CLOUD_PROVIDER]: CLOUD_PROVIDER_VALUE_AZURE,
54+
[ATTR_CLOUD_PLATFORM]: CLOUD_PLATFORM_VALUE_AZURE_AKS,
55+
};
56+
57+
if (metadata.name) {
58+
attributes = {
59+
...attributes,
60+
[ATTR_K8S_CLUSTER_NAME]: metadata.name,
61+
};
62+
}
63+
64+
if (metadata.resourceId) {
65+
attributes = {
66+
...attributes,
67+
[CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE]: metadata.resourceId,
68+
};
69+
}
70+
71+
diag.debug('AzureAksDetector: detected AKS cluster:', metadata);
72+
}
73+
74+
return { attributes };
75+
}
76+
77+
private getAksMetadata(): AksClusterMetadata | undefined {
78+
// Try environment variable first (populated from aks-cluster-metadata ConfigMap)
79+
const clusterResourceId = process.env[AKS_CLUSTER_RESOURCE_ID];
80+
81+
if (clusterResourceId) {
82+
return {
83+
name: extractClusterNameFromResourceId(clusterResourceId),
84+
resourceId: clusterResourceId,
85+
};
86+
}
87+
88+
// Fall back to reading from mounted ConfigMap file
89+
return this.getAksMetadataFromFile();
90+
}
91+
92+
private getAksMetadataFromFile(): AksClusterMetadata | undefined {
93+
try {
94+
if (!fs.existsSync(AKS_METADATA_FILE_PATH)) {
95+
return undefined;
96+
}
97+
98+
const content = fs.readFileSync(AKS_METADATA_FILE_PATH, 'utf8');
99+
const metadata: AksClusterMetadata = {};
100+
101+
// Parse the ConfigMap file content (key=value format)
102+
// The native aks-cluster-metadata ConfigMap has a single key: clusterResourceId
103+
const lines = content.split('\n');
104+
for (const line of lines) {
105+
const trimmedLine = line.trim();
106+
if (!trimmedLine || trimmedLine.startsWith('#')) {
107+
continue;
108+
}
109+
110+
const [key, ...valueParts] = trimmedLine.split('=');
111+
const value = valueParts.join('=').trim();
112+
113+
if (key === 'clusterResourceId' && value) {
114+
metadata.resourceId = value;
115+
metadata.name = extractClusterNameFromResourceId(value);
116+
}
117+
}
118+
119+
if (metadata.resourceId) {
120+
return metadata;
121+
}
122+
} catch (err: any) {
123+
diag.debug(
124+
'AzureAksDetector: failed to read AKS metadata file:',
125+
err.message
126+
);
127+
}
128+
129+
return undefined;
130+
}
131+
}
132+
133+
export const azureAksDetector = new AzureAksDetector();

packages/resource-detector-azure/src/detectors/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
export { azureAksDetector } from './AzureAksDetector';
1718
export { azureAppServiceDetector } from './AzureAppServiceDetector';
1819
export { azureFunctionsDetector } from './AzureFunctionsDetector';
1920
export { azureVmDetector } from './AzureVmDetector';

packages/resource-detector-azure/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
export {
18+
azureAksDetector,
1819
azureAppServiceDetector,
1920
azureFunctionsDetector,
2021
azureVmDetector,

packages/resource-detector-azure/src/semconv.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ export const ATTR_CLOUD_REGION = 'cloud.region' as const;
6767
export const ATTR_DEPLOYMENT_ENVIRONMENT_NAME =
6868
'deployment.environment.name' as const;
6969

70+
/**
71+
* The name of the cluster.
72+
*
73+
* @example opentelemetry-cluster
74+
*
75+
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
76+
*/
77+
export const ATTR_K8S_CLUSTER_NAME = 'k8s.cluster.name' as const;
78+
7079
/**
7180
* The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version.
7281
*
@@ -199,6 +208,15 @@ export const CLOUD_PLATFORM_VALUE_AZURE_FUNCTIONS = 'azure.functions' as const;
199208
*/
200209
export const CLOUD_PLATFORM_VALUE_AZURE_VM = 'azure.vm' as const;
201210

211+
/**
212+
* Enum value "azure.aks" for attribute {@link ATTR_CLOUD_PLATFORM}.
213+
*
214+
* Azure Kubernetes Service
215+
*
216+
* @experimental This enum value is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
217+
*/
218+
export const CLOUD_PLATFORM_VALUE_AZURE_AKS = 'azure.aks' as const;
219+
202220
/**
203221
* Enum value "azure" for attribute {@link ATTR_CLOUD_PROVIDER}.
204222
*

packages/resource-detector-azure/src/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,44 @@ export const AZURE_VM_METADATA_PATH =
3636
export const AZURE_VM_SCALE_SET_NAME_ATTRIBUTE = 'azure.vm.scaleset.name';
3737
export const AZURE_VM_SKU_ATTRIBUTE = 'azure.vm.sku';
3838

39+
// AKS ConfigMap environment variables
40+
// The native AKS ConfigMap is 'aks-cluster-metadata' in 'kube-public' namespace
41+
// with a single key 'clusterResourceId'
42+
export const AKS_CLUSTER_RESOURCE_ID = 'CLUSTER_RESOURCE_ID';
43+
44+
// AKS ConfigMap file path (mounted from aks-cluster-metadata ConfigMap in kube-public)
45+
export const AKS_METADATA_FILE_PATH = '/etc/kubernetes/aks-cluster-metadata';
46+
47+
export interface AksClusterMetadata {
48+
name?: string;
49+
resourceId?: string;
50+
}
51+
52+
/**
53+
* Extracts the cluster name from an AKS ARM resource ID.
54+
* @param resourceId The full ARM resource ID, e.g.:
55+
* /subscriptions/.../resourceGroups/.../providers/Microsoft.ContainerService/managedClusters/my-cluster
56+
* @returns The cluster name (last segment), or undefined if not found
57+
*/
58+
export function extractClusterNameFromResourceId(
59+
resourceId: string
60+
): string | undefined {
61+
if (!resourceId) return undefined;
62+
const segments = resourceId.split('/');
63+
// The cluster name is the last segment after 'managedClusters'
64+
const managedClustersIndex = segments.findIndex(
65+
s => s.toLowerCase() === 'managedclusters'
66+
);
67+
if (
68+
managedClustersIndex !== -1 &&
69+
managedClustersIndex < segments.length - 1
70+
) {
71+
return segments[managedClustersIndex + 1];
72+
}
73+
// Fallback: just return the last segment
74+
return segments[segments.length - 1] || undefined;
75+
}
76+
3977
export interface AzureVmMetadata {
4078
azEnvironment?: string;
4179
additionalCapabilities?: {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import { azureAksDetector } from '../../src/detectors/AzureAksDetector';
19+
import {
20+
ATTR_CLOUD_PLATFORM,
21+
ATTR_CLOUD_PROVIDER,
22+
ATTR_K8S_CLUSTER_NAME,
23+
} from '../../src/semconv';
24+
import { CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE } from '../../src/types';
25+
import { detectResources } from '@opentelemetry/resources';
26+
27+
describe('AzureAksDetector', () => {
28+
let originalEnv: NodeJS.ProcessEnv;
29+
30+
beforeEach(() => {
31+
originalEnv = process.env;
32+
process.env = { ...originalEnv };
33+
});
34+
35+
afterEach(() => {
36+
process.env = originalEnv;
37+
});
38+
39+
it('should detect AKS environment from CLUSTER_RESOURCE_ID environment variable', () => {
40+
process.env.CLUSTER_RESOURCE_ID =
41+
'/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-aks-cluster';
42+
43+
const resource = detectResources({
44+
detectors: [azureAksDetector],
45+
});
46+
47+
assert.ok(resource);
48+
const attributes = resource.attributes;
49+
assert.strictEqual(attributes[ATTR_CLOUD_PROVIDER], 'azure');
50+
assert.strictEqual(attributes[ATTR_CLOUD_PLATFORM], 'azure.aks');
51+
assert.strictEqual(attributes[ATTR_K8S_CLUSTER_NAME], 'test-aks-cluster');
52+
assert.strictEqual(
53+
attributes[CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE],
54+
'/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-aks-cluster'
55+
);
56+
});
57+
58+
it('should extract cluster name from resource ID with different casing', () => {
59+
process.env.CLUSTER_RESOURCE_ID =
60+
'/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.ContainerService/ManagedClusters/my-cluster-name';
61+
62+
const resource = detectResources({
63+
detectors: [azureAksDetector],
64+
});
65+
66+
assert.ok(resource);
67+
const attributes = resource.attributes;
68+
assert.strictEqual(attributes[ATTR_CLOUD_PROVIDER], 'azure');
69+
assert.strictEqual(attributes[ATTR_CLOUD_PLATFORM], 'azure.aks');
70+
assert.strictEqual(attributes[ATTR_K8S_CLUSTER_NAME], 'my-cluster-name');
71+
});
72+
73+
it('should return empty resource when not in AKS environment', () => {
74+
delete process.env.CLUSTER_RESOURCE_ID;
75+
76+
const resource = detectResources({
77+
detectors: [azureAksDetector],
78+
});
79+
80+
assert.ok(resource);
81+
const attributes = resource.attributes;
82+
assert.strictEqual(attributes[ATTR_CLOUD_PROVIDER], undefined);
83+
assert.strictEqual(attributes[ATTR_CLOUD_PLATFORM], undefined);
84+
assert.strictEqual(attributes[ATTR_K8S_CLUSTER_NAME], undefined);
85+
assert.strictEqual(
86+
attributes[CLOUD_RESOURCE_ID_RESOURCE_ATTRIBUTE],
87+
undefined
88+
);
89+
});
90+
});

0 commit comments

Comments
 (0)