diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln
index 936e6672f8..3c91bad51a 100644
--- a/opentelemetry-dotnet-contrib.sln
+++ b/opentelemetry-dotnet-contrib.sln
@@ -240,6 +240,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{1FCC8E
ProjectSection(SolutionItems) = preProject
src\Shared\ActivityInstrumentationHelper.cs = src\Shared\ActivityInstrumentationHelper.cs
src\Shared\AssemblyVersionExtensions.cs = src\Shared\AssemblyVersionExtensions.cs
+ src\Shared\AsyncHelper.cs = src\Shared\AsyncHelper.cs
src\Shared\DatabaseSemanticConventionHelper.cs = src\Shared\DatabaseSemanticConventionHelper.cs
src\Shared\DiagnosticSourceListener.cs = src\Shared\DiagnosticSourceListener.cs
src\Shared\DiagnosticSourceSubscriber.cs = src\Shared\DiagnosticSourceSubscriber.cs
@@ -255,6 +256,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{1FCC8E
src\Shared\PropertyFetcher.AOT.cs = src\Shared\PropertyFetcher.AOT.cs
src\Shared\PropertyFetcher.cs = src\Shared\PropertyFetcher.cs
src\Shared\RedactionHelper.cs = src\Shared\RedactionHelper.cs
+ src\Shared\ResourceDetectorUtils.cs = src\Shared\ResourceDetectorUtils.cs
src\Shared\ResourceSemanticConventions.cs = src\Shared\ResourceSemanticConventions.cs
src\Shared\SemanticConventions.cs = src\Shared\SemanticConventions.cs
src\Shared\ServerCertificateValidationHandler.cs = src\Shared\ServerCertificateValidationHandler.cs
diff --git a/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj b/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj
index 6c9fe6bf60..4fc595e4ac 100644
--- a/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj
+++ b/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj
@@ -24,9 +24,11 @@
+
+
diff --git a/src/OpenTelemetry.Resources.Container/CHANGELOG.md b/src/OpenTelemetry.Resources.Container/CHANGELOG.md
index a8ebb3af83..d6e252c943 100644
--- a/src/OpenTelemetry.Resources.Container/CHANGELOG.md
+++ b/src/OpenTelemetry.Resources.Container/CHANGELOG.md
@@ -13,6 +13,9 @@ Released 2024-Dec-09
* Updated OpenTelemetry core component version(s) to `1.10.0`.
([#2317](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2317))
+* Add Kubernetes support.
+ ([#1699](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1699))
+
## 1.0.0-beta.9
Released 2024-Jun-18
diff --git a/src/OpenTelemetry.Resources.Container/ContainerDetector.cs b/src/OpenTelemetry.Resources.Container/ContainerDetector.cs
index 0f43e1495f..eb9974677a 100644
--- a/src/OpenTelemetry.Resources.Container/ContainerDetector.cs
+++ b/src/OpenTelemetry.Resources.Container/ContainerDetector.cs
@@ -14,21 +14,25 @@ internal sealed class ContainerDetector : IResourceDetector
private const string Filepath = "/proc/self/cgroup";
private const string FilepathV2 = "/proc/self/mountinfo";
private const string Hostname = "hostname";
+ private const string K8sCertificatePath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
+
+ private readonly IK8sMetadataFetcher k8sMetadataFetcher;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContainerDetector()
+ : this(new K8sMetadataFetcher())
+ {
+ }
///
- /// CGroup Parse Versions.
+ /// Initializes a new instance of the class for testing.
///
- internal enum ParseMode
+ /// The .
+ internal ContainerDetector(IK8sMetadataFetcher k8sMetadataFetcher)
{
- ///
- /// Represents CGroupV1.
- ///
- V1,
-
- ///
- /// Represents CGroupV2.
- ///
- V2,
+ this.k8sMetadataFetcher = k8sMetadataFetcher;
}
///
@@ -37,26 +41,78 @@ internal enum ParseMode
/// Resource with key-value pairs of resource attributes.
public Resource Detect()
{
- var cGroupBuild = this.BuildResource(Filepath, ParseMode.V1);
- if (cGroupBuild == Resource.Empty)
+ var containerId = this.ExtractK8sContainerId();
+ if (!string.IsNullOrEmpty(containerId))
{
- cGroupBuild = this.BuildResource(FilepathV2, ParseMode.V2);
+ return BuildResource(containerId);
}
- return cGroupBuild;
+ containerId = this.ExtractContainerId(Filepath, ParseMode.V1);
+ if (!string.IsNullOrEmpty(containerId))
+ {
+ return BuildResource(containerId);
+ }
+
+ containerId = this.ExtractContainerId(FilepathV2, ParseMode.V2);
+ if (!string.IsNullOrEmpty(containerId))
+ {
+ return BuildResource(containerId);
+ }
+
+ return Resource.Empty;
+
+ static Resource BuildResource(string containerId)
+ {
+ return new Resource(new List>(1) { new(ContainerSemanticConventions.AttributeContainerId, containerId!) });
+ }
}
///
- /// Builds the resource attributes from Container Id in file path.
+ /// Extracts Container Id from path using the cgroupv1 format.
///
- /// File path where container id exists.
- /// CGroup Version of file to parse from.
- /// Returns Resource with list of key-value pairs of container resource attributes if container id exists else empty resource.
- internal Resource BuildResource(string path, ParseMode cgroupVersion)
+ /// cgroup path.
+ /// CGroup Version of file to parse from.
+ /// Container Id, if not found or exception being thrown.
+ internal string? ExtractContainerId(string path, ParseMode parseMode)
{
- var containerId = this.ExtractContainerId(path, cgroupVersion);
+ try
+ {
+ if (!File.Exists(path))
+ {
+ return null;
+ }
+
+ foreach (string line in File.ReadLines(path))
+ {
+ string? containerId = null;
+ if (!string.IsNullOrEmpty(line))
+ {
+ if (parseMode == ParseMode.V1)
+ {
+ containerId = GetIdFromLineV1(line);
+ }
+#if NET
+ else if (parseMode == ParseMode.V2 && line.Contains(Hostname, StringComparison.Ordinal))
+#else
+ else if (parseMode == ParseMode.V2 && line.Contains(Hostname))
+#endif
+ {
+ containerId = GetIdFromLineV2(line);
+ }
+ }
- return string.IsNullOrEmpty(containerId) ? Resource.Empty : new Resource([new(ContainerSemanticConventions.AttributeContainerId, containerId!)]);
+ if (!string.IsNullOrEmpty(containerId))
+ {
+ return containerId;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ ContainerResourceEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerDetector)} : Failed to extract Container id from path", ex);
+ }
+
+ return null;
}
///
@@ -102,7 +158,6 @@ internal Resource BuildResource(string path, ParseMode cgroupVersion)
private static string RemovePrefixAndSuffixIfNeeded(string input, int startIndex, int endIndex)
{
startIndex = (startIndex == -1) ? 0 : startIndex + 1;
-
if (endIndex == -1)
{
endIndex = input.Length;
@@ -111,49 +166,47 @@ private static string RemovePrefixAndSuffixIfNeeded(string input, int startIndex
return input.Substring(startIndex, endIndex - startIndex);
}
- ///
- /// Extracts Container Id from path using the cgroupv1 format.
- ///
- /// cgroup path.
- /// CGroup Version of file to parse from.
- /// Container Id, Null if not found or exception being thrown.
- private string? ExtractContainerId(string path, ParseMode cgroupVersion)
+ private string? ExtractK8sContainerId()
{
try
{
- if (!File.Exists(path))
+ var baseUrl = this.k8sMetadataFetcher.GetServiceBaseUrl();
+ var containerName = this.k8sMetadataFetcher.GetContainerName();
+ if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(containerName))
{
return null;
}
- foreach (var line in File.ReadLines(path))
+ var @namespace = this.k8sMetadataFetcher.GetNamespace();
+ var hostname = this.k8sMetadataFetcher.GetPodName() ?? this.k8sMetadataFetcher.GetHostname();
+ var url = $"{baseUrl}/api/v1/namespaces/{@namespace}/pods/{hostname}";
+ var credentials = this.k8sMetadataFetcher.GetApiCredential();
+ if (string.IsNullOrEmpty(credentials))
{
- string? containerId = null;
- if (!string.IsNullOrEmpty(line))
- {
- if (cgroupVersion == ParseMode.V1)
- {
- containerId = GetIdFromLineV1(line);
- }
-#if NET
- else if (cgroupVersion == ParseMode.V2 && line.Contains(Hostname, StringComparison.Ordinal))
-#else
- else if (cgroupVersion == ParseMode.V2 && line.Contains(Hostname))
-#endif
- {
- containerId = GetIdFromLineV2(line);
- }
- }
+ return null;
+ }
- if (!string.IsNullOrEmpty(containerId))
- {
- return containerId;
- }
+ using var httpClientHandler = ServerCertificateValidationHandler.Create(K8sCertificatePath, ContainerResourceEventSource.Log);
+ var response = AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(url, HttpMethod.Get, new KeyValuePair("Authorization", credentials), httpClientHandler));
+ var pod = ResourceDetectorUtils.DeserializeFromString(response, SourceGenerationContext.Default.K8sPod);
+ if (pod?.Status?.ContainerStatuses == null)
+ {
+ return null;
}
+
+ var container = pod.Status.ContainerStatuses.SingleOrDefault(p => p.Name == containerName);
+ if (string.IsNullOrEmpty(container?.Id))
+ {
+ return null;
+ }
+
+ // Container's ID is in :// format.
+ var index = container.Id.LastIndexOf('/');
+ return container.Id.Substring(index + 1);
}
catch (Exception ex)
{
- ContainerExtensionsEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerDetector)} : Failed to extract Container id from path", ex);
+ ContainerResourceEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerDetector)}: Failed to extract container id", ex);
}
return null;
diff --git a/src/OpenTelemetry.Resources.Container/ContainerExtensionsEventSource.cs b/src/OpenTelemetry.Resources.Container/ContainerExtensionsEventSource.cs
deleted file mode 100644
index 8efa496e6e..0000000000
--- a/src/OpenTelemetry.Resources.Container/ContainerExtensionsEventSource.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright The OpenTelemetry Authors
-// SPDX-License-Identifier: Apache-2.0
-
-using System.Diagnostics.Tracing;
-using OpenTelemetry.Internal;
-
-namespace OpenTelemetry.Resources.Container;
-
-[EventSource(Name = "OpenTelemetry-Resources-Container")]
-internal class ContainerExtensionsEventSource : EventSource
-{
- public static ContainerExtensionsEventSource Log = new();
-
- [NonEvent]
- public void ExtractResourceAttributesException(string format, Exception ex)
- {
- if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1)))
- {
- this.FailedToExtractResourceAttributes(format, ex.ToInvariantString());
- }
- }
-
- [Event(1, Message = "Failed to extract resource attributes in '{0}'.", Level = EventLevel.Error)]
- public void FailedToExtractResourceAttributes(string format, string exception)
- {
- this.WriteEvent(1, format, exception);
- }
-}
diff --git a/src/OpenTelemetry.Resources.Container/ContainerResourceEventSource.cs b/src/OpenTelemetry.Resources.Container/ContainerResourceEventSource.cs
new file mode 100644
index 0000000000..5283956384
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/ContainerResourceEventSource.cs
@@ -0,0 +1,58 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics.Tracing;
+using OpenTelemetry.Internal;
+
+namespace OpenTelemetry.Resources.Container;
+
+[EventSource(Name = "OpenTelemetry-Resources-Container")]
+internal sealed class ContainerResourceEventSource : EventSource, IServerCertificateValidationEventSource
+{
+ public static ContainerResourceEventSource Log = new();
+
+ private const int EventIdFailedToExtractResourceAttributes = 1;
+ private const int EventIdFailedToValidateCertificate = 2;
+ private const int EventIdFailedToCreateHttpHandler = 3;
+ private const int EventIdFailedCertificateFileNotExists = 4;
+ private const int EventIdFailedToLoadCertificateInStorage = 5;
+
+ [NonEvent]
+ public void ExtractResourceAttributesException(string format, Exception ex)
+ {
+ if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1)))
+ {
+ this.FailedToExtractResourceAttributes(format, ex.ToInvariantString());
+ }
+ }
+
+ [Event(EventIdFailedToExtractResourceAttributes, Message = "Failed to extract resource attributes in '{0}'.", Level = EventLevel.Error)]
+ public void FailedToExtractResourceAttributes(string format, string exception)
+ {
+ this.WriteEvent(EventIdFailedToExtractResourceAttributes, format, exception);
+ }
+
+ [Event(EventIdFailedToValidateCertificate, Message = "Failed to validate certificate. Details: '{0}'", Level = EventLevel.Warning)]
+ public void FailedToValidateCertificate(string error)
+ {
+ this.WriteEvent(EventIdFailedToValidateCertificate, error);
+ }
+
+ [Event(EventIdFailedToCreateHttpHandler, Message = "Failed to create HTTP handler. Exception: '{0}'", Level = EventLevel.Warning)]
+ public void FailedToCreateHttpHandler(Exception exception)
+ {
+ this.WriteEvent(EventIdFailedToCreateHttpHandler, exception.ToInvariantString());
+ }
+
+ [Event(EventIdFailedCertificateFileNotExists, Message = "Certificate file does not exist. File: '{0}'", Level = EventLevel.Warning)]
+ public void CertificateFileDoesNotExist(string filename)
+ {
+ this.WriteEvent(EventIdFailedCertificateFileNotExists, filename);
+ }
+
+ [Event(EventIdFailedToLoadCertificateInStorage, Message = "Failed to load certificate in trusted storage. File: '{0}'", Level = EventLevel.Warning)]
+ public void FailedToLoadCertificateInTrustedStorage(string filename)
+ {
+ this.WriteEvent(EventIdFailedToLoadCertificateInStorage, filename);
+ }
+}
diff --git a/src/OpenTelemetry.Resources.Container/IK8sMetadataFetcher.cs b/src/OpenTelemetry.Resources.Container/IK8sMetadataFetcher.cs
new file mode 100644
index 0000000000..72992fcc2b
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/IK8sMetadataFetcher.cs
@@ -0,0 +1,19 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.Resources.Container;
+
+internal interface IK8sMetadataFetcher
+{
+ string? GetApiCredential();
+
+ string? GetContainerName();
+
+ string? GetHostname();
+
+ string? GetPodName();
+
+ string? GetNamespace();
+
+ string? GetServiceBaseUrl();
+}
diff --git a/src/OpenTelemetry.Resources.Container/K8sMetadataFetcher.cs b/src/OpenTelemetry.Resources.Container/K8sMetadataFetcher.cs
new file mode 100644
index 0000000000..2ff6c58d05
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/K8sMetadataFetcher.cs
@@ -0,0 +1,74 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text;
+
+namespace OpenTelemetry.Resources.Container;
+
+internal sealed class K8sMetadataFetcher : IK8sMetadataFetcher
+{
+ private const string KubernetesServiceHostEnvVar = "KUBERNETES_SERVICE_HOST";
+ private const string KubernetesServicePortEnvVar = "KUBERNETES_SERVICE_PORT_HTTPS";
+ private const string KubernetesHostnameEnvVar = "HOSTNAME";
+ private const string KubernetesPodNameEnvVar = "KUBERNETES_POD_NAME";
+ private const string KubernetesContainerNameEnvVar = "KUBERNETES_CONTAINER_NAME";
+ private const string KubernetesNamespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
+ private const string KubernetesCredentialPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";
+
+ public string? GetApiCredential()
+ {
+ try
+ {
+ var stringBuilder = new StringBuilder("Bearer ");
+
+ using (var streamReader = ResourceDetectorUtils.GetStreamReader(KubernetesCredentialPath))
+ {
+ while (!streamReader.EndOfStream)
+ {
+ _ = stringBuilder.Append(streamReader.ReadLine()?.Trim());
+ }
+ }
+
+ return stringBuilder.ToString();
+ }
+ catch (Exception ex)
+ {
+ ContainerResourceEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerDetector)}: Failed to load client token", ex);
+ }
+
+ return null;
+ }
+
+ public string? GetContainerName()
+ {
+ return Environment.GetEnvironmentVariable(KubernetesContainerNameEnvVar);
+ }
+
+ public string? GetHostname()
+ {
+ return Environment.GetEnvironmentVariable(KubernetesHostnameEnvVar);
+ }
+
+ public string? GetPodName()
+ {
+ return Environment.GetEnvironmentVariable(KubernetesPodNameEnvVar);
+ }
+
+ public string? GetNamespace()
+ {
+ return File.ReadAllText(KubernetesNamespacePath);
+ }
+
+ public string? GetServiceBaseUrl()
+ {
+ var serviceHost = Environment.GetEnvironmentVariable(KubernetesServiceHostEnvVar);
+ var servicePort = Environment.GetEnvironmentVariable(KubernetesServicePortEnvVar);
+
+ if (string.IsNullOrWhiteSpace(serviceHost) || string.IsNullOrWhiteSpace(servicePort))
+ {
+ return null;
+ }
+
+ return $"https://{serviceHost}:{servicePort}";
+ }
+}
diff --git a/src/OpenTelemetry.Resources.Container/Models/K8sContainerStatus.cs b/src/OpenTelemetry.Resources.Container/Models/K8sContainerStatus.cs
new file mode 100644
index 0000000000..22fc260562
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/Models/K8sContainerStatus.cs
@@ -0,0 +1,15 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.Json.Serialization;
+
+namespace OpenTelemetry.Resources.Container.Models;
+
+internal sealed class K8sContainerStatus
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = default!;
+
+ [JsonPropertyName("containerID")]
+ public string Id { get; set; } = default!;
+}
diff --git a/src/OpenTelemetry.Resources.Container/Models/K8sPod.cs b/src/OpenTelemetry.Resources.Container/Models/K8sPod.cs
new file mode 100644
index 0000000000..681cbe3409
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/Models/K8sPod.cs
@@ -0,0 +1,12 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.Json.Serialization;
+
+namespace OpenTelemetry.Resources.Container.Models;
+
+internal sealed class K8sPod
+{
+ [JsonPropertyName("status")]
+ public K8sPodStatus? Status { get; set; }
+}
diff --git a/src/OpenTelemetry.Resources.Container/Models/K8sPodStatus.cs b/src/OpenTelemetry.Resources.Container/Models/K8sPodStatus.cs
new file mode 100644
index 0000000000..7451635af3
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/Models/K8sPodStatus.cs
@@ -0,0 +1,12 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.Json.Serialization;
+
+namespace OpenTelemetry.Resources.Container.Models;
+
+internal sealed class K8sPodStatus
+{
+ [JsonPropertyName("containerStatuses")]
+ public IReadOnlyList ContainerStatuses { get; set; } = new List();
+}
diff --git a/src/OpenTelemetry.Resources.Container/OpenTelemetry.Resources.Container.csproj b/src/OpenTelemetry.Resources.Container/OpenTelemetry.Resources.Container.csproj
index 6bef037057..a244ed78ca 100644
--- a/src/OpenTelemetry.Resources.Container/OpenTelemetry.Resources.Container.csproj
+++ b/src/OpenTelemetry.Resources.Container/OpenTelemetry.Resources.Container.csproj
@@ -7,6 +7,11 @@
Resources.Container-
+
+
+ $(NoWarn);nullable
+
+
@@ -15,11 +20,17 @@
+
+
+
+
+
+
diff --git a/src/OpenTelemetry.Resources.Container/ParseMode.cs b/src/OpenTelemetry.Resources.Container/ParseMode.cs
new file mode 100644
index 0000000000..5a3ce66bf7
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/ParseMode.cs
@@ -0,0 +1,20 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.Resources.Container;
+
+///
+/// CGroup Parse Versions.
+///
+internal enum ParseMode
+{
+ ///
+ /// Represents CGroupV1.
+ ///
+ V1,
+
+ ///
+ /// Represents CGroupV2.
+ ///
+ V2,
+}
diff --git a/src/OpenTelemetry.Resources.Container/README.md b/src/OpenTelemetry.Resources.Container/README.md
index 9cb0c6ce3c..4a0ab04514 100644
--- a/src/OpenTelemetry.Resources.Container/README.md
+++ b/src/OpenTelemetry.Resources.Container/README.md
@@ -52,6 +52,75 @@ your application is running:
- **ContainerDetector**: container.id.
+## Kubernetes
+
+To make container ID resolution work, container and pod name should be provided
+through `KUBERNETES_CONTAINER_NAME` and `KUBERNETES_POD_NAME` environment variable
+respectively and pod should have at least
+get permission to kubernetes resource pods.
+It can be done by utilizing YAML anchoring, downwards API
+and RBAC (Role-Based Access Control).
+
+If `KUBERNETES_POD_NAME` is not provided, detector will use `HOSTNAME`
+as a fallback, but it may not work in some environments
+or if hostname was overridden in pod spec.
+
+Below is an example of how to configure sample pod
+to make container ID resolution working:
+
+```yaml
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: pod-reader-account
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ namespace: default
+ name: pod-reader
+rules:
+- apiGroups: [""]
+ resources: ["pods"]
+ verbs: ["get"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: read-pods
+ namespace: default
+subjects:
+- kind: ServiceAccount
+ name: pod-reader-account
+ namespace: default
+roleRef:
+ kind: Role
+ name: pod-reader
+ apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: Pod
+metadata:
+ name: container-resolve-demo
+spec:
+ serviceAccountName: pod-reader-account
+ volumes:
+ - name: shared-data
+ emptyDir: {}
+ containers:
+ - name: &container_name my_container_name
+ image: ubuntu:latest
+ command: [ "/bin/bash", "-c", "--" ]
+ args: [ "while true; do sleep 30; done;" ]
+ env:
+ - name: KUBERNETES_CONTAINER_NAME
+ value: *container_name
+ - name: KUBERNETES_POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+```
+
## References
- [OpenTelemetry Project](https://opentelemetry.io/)
diff --git a/src/OpenTelemetry.Resources.Container/SourceGenerationContext.cs b/src/OpenTelemetry.Resources.Container/SourceGenerationContext.cs
new file mode 100644
index 0000000000..22cb03deb1
--- /dev/null
+++ b/src/OpenTelemetry.Resources.Container/SourceGenerationContext.cs
@@ -0,0 +1,22 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Text.Json.Serialization;
+using OpenTelemetry.Resources.Container.Models;
+
+namespace OpenTelemetry.Resources.Container;
+
+///
+/// "Source Generation" is feature added to System.Text.Json in .NET 6.0.
+/// This is a performance optimization that avoids runtime reflection when performing serialization.
+/// Serialization metadata will be computed at compile-time and included in the assembly.
+/// .
+/// .
+/// .
+///
+[JsonSerializable(typeof(K8sPod))]
+[JsonSerializable(typeof(K8sPodStatus))]
+[JsonSerializable(typeof(K8sContainerStatus))]
+internal sealed partial class SourceGenerationContext : JsonSerializerContext
+{
+}
diff --git a/src/OpenTelemetry.Resources.AWS/AsyncHelper.cs b/src/Shared/AsyncHelper.cs
similarity index 97%
rename from src/OpenTelemetry.Resources.AWS/AsyncHelper.cs
rename to src/Shared/AsyncHelper.cs
index a75d5579c9..9b0782b2f0 100644
--- a/src/OpenTelemetry.Resources.AWS/AsyncHelper.cs
+++ b/src/Shared/AsyncHelper.cs
@@ -1,7 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
-namespace OpenTelemetry.Resources.AWS;
+namespace OpenTelemetry.Resources;
///
/// A helper class for running asynchronous methods synchronously.
diff --git a/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs b/src/Shared/ResourceDetectorUtils.cs
similarity index 98%
rename from src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs
rename to src/Shared/ResourceDetectorUtils.cs
index b870fd499d..5b89c51cfb 100644
--- a/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs
+++ b/src/Shared/ResourceDetectorUtils.cs
@@ -10,7 +10,7 @@
using System.Text.Json.Serialization.Metadata;
#endif
-namespace OpenTelemetry.Resources.AWS;
+namespace OpenTelemetry.Resources;
///
/// Class for resource detector utils.
diff --git a/test/OpenTelemetry.Resources.Container.Tests/ContainerDetectorTests.cs b/test/OpenTelemetry.Resources.Container.Tests/ContainerDetectorTests.cs
index 47b43b329b..3928947fa7 100644
--- a/test/OpenTelemetry.Resources.Container.Tests/ContainerDetectorTests.cs
+++ b/test/OpenTelemetry.Resources.Container.Tests/ContainerDetectorTests.cs
@@ -1,6 +1,11 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
+using System.Text;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Hosting.Server.Features;
+using Microsoft.AspNetCore.Http;
using Xunit;
namespace OpenTelemetry.Resources.Container.Tests;
@@ -13,25 +18,25 @@ public class ContainerDetectorTests
name: "cgroupv1 with prefix",
line: "13:name=systemd:/podruntime/docker/kubepods/crio-e2cc29debdf85dde404998aa128997a819ff",
expectedContainerId: "e2cc29debdf85dde404998aa128997a819ff",
- cgroupVersion: ContainerDetector.ParseMode.V1),
+ cgroupVersion: ParseMode.V1),
new(
name: "cgroupv1 with suffix",
line: "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23.aaaa",
expectedContainerId: "ac679f8a8319c8cf7d38e1adf263bc08d23",
- cgroupVersion: ContainerDetector.ParseMode.V1),
+ cgroupVersion: ParseMode.V1),
new(
name: "cgroupv1 with prefix and suffix",
line: "13:name=systemd:/podruntime/docker/kubepods/crio-dc679f8a8319c8cf7d38e1adf263bc08d23.stuff",
expectedContainerId: "dc679f8a8319c8cf7d38e1adf263bc08d23",
- cgroupVersion: ContainerDetector.ParseMode.V1),
+ cgroupVersion: ParseMode.V1),
new(
name: "cgroupv1 with container Id",
line: "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
expectedContainerId: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
- cgroupVersion: ContainerDetector.ParseMode.V1)
+ cgroupVersion: ParseMode.V1)
];
@@ -41,37 +46,37 @@ public class ContainerDetectorTests
name: "cgroupv2 with container Id",
line: "13:name=systemd:/pod/d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356/hostname",
expectedContainerId: "d86d75589bf6cc254f3e2cc29debdf85dde404998aa128997a819ff991827356",
- cgroupVersion: ContainerDetector.ParseMode.V2),
+ cgroupVersion: ParseMode.V2),
new(
name: "cgroupv2 with full line",
line: "473 456 254:1 /docker/containers/dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw",
expectedContainerId: "dc64b5743252dbaef6e30521c34d6bbd1620c8ce65bdb7bf9e7143b61bb5b183",
- cgroupVersion: ContainerDetector.ParseMode.V2),
+ cgroupVersion: ParseMode.V2),
new(
name: "cgroupv2 with minikube containerd mountinfo",
line: "1537 1517 8:1 /var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/fb5916a02feca96bdeecd8e062df9e5e51d6617c8214b5e1f3ff9320f4402ae6/hostname /etc/hostname rw,relatime - ext4 /dev/sda1 rw",
expectedContainerId: "fb5916a02feca96bdeecd8e062df9e5e51d6617c8214b5e1f3ff9320f4402ae6",
- cgroupVersion: ContainerDetector.ParseMode.V2),
+ cgroupVersion: ParseMode.V2),
new(
name: "cgroupv2 with minikube docker mountinfo",
line: "2327 2307 8:1 /var/lib/docker/containers/a1551a1d7e1881d6c18d2c9ec462cab6ad3666825f0adb2098e9d5b198fd7e19/hostname /etc/hostname rw,relatime - ext4 /dev/sda1 rw",
expectedContainerId: "a1551a1d7e1881d6c18d2c9ec462cab6ad3666825f0adb2098e9d5b198fd7e19",
- cgroupVersion: ContainerDetector.ParseMode.V2),
+ cgroupVersion: ParseMode.V2),
new(
name: "cgroupv2 with minikube docker mountinfo2",
line: "929 920 254:1 /docker/volumes/minikube/_data/lib/docker/containers/0eaa6718003210b6520f7e82d14b4c8d4743057a958a503626240f8d1900bc33/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw",
expectedContainerId: "0eaa6718003210b6520f7e82d14b4c8d4743057a958a503626240f8d1900bc33",
- cgroupVersion: ContainerDetector.ParseMode.V2),
+ cgroupVersion: ParseMode.V2),
new(
name: "cgroupv2 with podman mountinfo",
line: "1096 1088 0:104 /containers/overlay-containers/1a2de27e7157106568f7e081e42a8c14858c02bd9df30d6e352b298178b46809/userdata/hostname /etc/hostname rw,nosuid,nodev,relatime - tmpfs tmpfs rw,size=813800k,nr_inodes=203450,mode=700,uid=1000,gid=1000",
expectedContainerId: "1a2de27e7157106568f7e081e42a8c14858c02bd9df30d6e352b298178b46809",
- cgroupVersion: ContainerDetector.ParseMode.V2)
+ cgroupVersion: ParseMode.V2)
];
@@ -80,12 +85,12 @@ public class ContainerDetectorTests
new(
name: "Invalid cgroupv1 line",
line: "13:name=systemd:/podruntime/docker/kubepods/ac679f8a8319c8cf7d38e1adf263bc08d23zzzz",
- cgroupVersion: ContainerDetector.ParseMode.V1),
+ cgroupVersion: ParseMode.V1),
new(
name: "Invalid hex cgroupv2 line (contains a z)",
line: "13:name=systemd:/var/lib/containerd/io.containerd.grpc.v1.cri/sandboxes/fb5916a02feca96bdeecd8e062df9e5e51d6617c8214b5e1f3fz9320f4402ae6/hostname",
- cgroupVersion: ContainerDetector.ParseMode.V2)
+ cgroupVersion: ParseMode.V2)
];
@@ -101,7 +106,7 @@ public void TestValidContainer()
tempFile.Write(testCase.Line);
Assert.Equal(
testCase.ExpectedContainerId,
- GetContainerId(containerDetector.BuildResource(tempFile.FilePath, testCase.CgroupVersion)));
+ containerDetector.ExtractContainerId(tempFile.FilePath, testCase.CgroupVersion));
}
}
@@ -115,9 +120,8 @@ public void TestInvalidContainer()
{
using var tempFile = new TempFile();
tempFile.Write(testCase.Line);
- Assert.Equal(
- containerDetector.BuildResource(tempFile.FilePath, ContainerDetector.ParseMode.V2),
- Resource.Empty);
+ Assert.Null(
+ containerDetector.ExtractContainerId(tempFile.FilePath, ParseMode.V2));
}
// Valid in cgroupv1 is not valid in cgroupv1
@@ -125,9 +129,8 @@ public void TestInvalidContainer()
{
using var tempFile = new TempFile();
tempFile.Write(testCase.Line);
- Assert.Equal(
- containerDetector.BuildResource(tempFile.FilePath, ContainerDetector.ParseMode.V1),
- Resource.Empty);
+ Assert.Null(
+ containerDetector.ExtractContainerId(tempFile.FilePath, ParseMode.V1));
}
// test invalid cases
@@ -135,23 +138,28 @@ public void TestInvalidContainer()
{
using var tempFile = new TempFile();
tempFile.Write(testCase.Line);
- Assert.Equal(containerDetector.BuildResource(tempFile.FilePath, testCase.CgroupVersion), Resource.Empty);
+ Assert.Null(containerDetector.ExtractContainerId(tempFile.FilePath, testCase.CgroupVersion));
}
// test invalid file
- Assert.Equal(containerDetector.BuildResource(Path.GetTempPath(), ContainerDetector.ParseMode.V1), Resource.Empty);
- Assert.Equal(containerDetector.BuildResource(Path.GetTempPath(), ContainerDetector.ParseMode.V2), Resource.Empty);
+ Assert.Null(containerDetector.ExtractContainerId(Path.GetTempPath(), ParseMode.V1));
+ Assert.Null(containerDetector.ExtractContainerId(Path.GetTempPath(), ParseMode.V2));
}
- private static string GetContainerId(Resource resource)
+ [Fact]
+ public async Task TestK8sContainerId()
{
- var resourceAttributes = resource.Attributes.ToDictionary(x => x.Key, x => x.Value);
- return resourceAttributes[ContainerSemanticConventions.AttributeContainerId].ToString()!;
+ await using (_ = new MockK8sEndpoint("k8s/pod-response.json"))
+ {
+ var resourceAttributes = new ContainerDetector(new MockK8sMetadataFetcher()).Detect().Attributes.ToDictionary(x => x.Key, x => x.Value);
+
+ Assert.Equal(resourceAttributes[ContainerSemanticConventions.AttributeContainerId], "96724c05fa1be8d313f6db0e9872ca542b076839c4fd51ea4912a670ef538cbd");
+ }
}
private sealed class TestCase
{
- public TestCase(string name, string line, ContainerDetector.ParseMode cgroupVersion, string? expectedContainerId = null)
+ public TestCase(string name, string line, ParseMode cgroupVersion, string? expectedContainerId = null)
{
this.Name = name;
this.Line = line;
@@ -165,6 +173,83 @@ public TestCase(string name, string line, ContainerDetector.ParseMode cgroupVers
public string? ExpectedContainerId { get; }
- public ContainerDetector.ParseMode CgroupVersion { get; }
+ public ParseMode CgroupVersion { get; }
+ }
+
+ private class MockK8sEndpoint : IAsyncDisposable
+ {
+ public readonly Uri Address;
+ private readonly IWebHost server;
+
+ public MockK8sEndpoint(string responseJsonPath)
+ {
+ this.server = new WebHostBuilder()
+ .UseKestrel()
+ .UseUrls("http://127.0.0.1:5000")
+ .Configure(app =>
+ {
+ app.Run(async context =>
+ {
+ if (context.Request.Method == HttpMethods.Get && context.Request.Path == "/api/v1/namespaces/default/pods/pod1")
+ {
+ var content = await File.ReadAllTextAsync($"{Environment.CurrentDirectory}/{responseJsonPath}");
+ var data = Encoding.UTF8.GetBytes(content);
+ context.Response.ContentType = "application/json";
+ await context.Response.Body.WriteAsync(data);
+ }
+ else
+ {
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
+ await context.Response.WriteAsync("Not found");
+ }
+ });
+ }).Build();
+ this.server.Start();
+
+ this.Address = new Uri(this.server.ServerFeatures.Get()!.Addresses.First());
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await this.DisposeAsyncCore();
+ }
+
+ protected virtual async ValueTask DisposeAsyncCore()
+ {
+ await this.server.StopAsync();
+ }
+ }
+
+ private sealed class MockK8sMetadataFetcher : IK8sMetadataFetcher
+ {
+ public string? GetApiCredential()
+ {
+ return Guid.NewGuid().ToString();
+ }
+
+ public string? GetContainerName()
+ {
+ return "my-container";
+ }
+
+ public string? GetHostname()
+ {
+ return "hostname";
+ }
+
+ public string? GetNamespace()
+ {
+ return "default";
+ }
+
+ public string? GetPodName()
+ {
+ return "pod1";
+ }
+
+ public string? GetServiceBaseUrl()
+ {
+ return "http://127.0.0.1:5000";
+ }
}
}
diff --git a/test/OpenTelemetry.Resources.Container.Tests/OpenTelemetry.Resources.Container.Tests.csproj b/test/OpenTelemetry.Resources.Container.Tests/OpenTelemetry.Resources.Container.Tests.csproj
index 27337cec83..f903e1b7ab 100644
--- a/test/OpenTelemetry.Resources.Container.Tests/OpenTelemetry.Resources.Container.Tests.csproj
+++ b/test/OpenTelemetry.Resources.Container.Tests/OpenTelemetry.Resources.Container.Tests.csproj
@@ -6,8 +6,18 @@
Unit test project for Container Detector for OpenTelemetry.
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/test/OpenTelemetry.Resources.Container.Tests/k8s/pod-response.json b/test/OpenTelemetry.Resources.Container.Tests/k8s/pod-response.json
new file mode 100644
index 0000000000..b82a3e228a
--- /dev/null
+++ b/test/OpenTelemetry.Resources.Container.Tests/k8s/pod-response.json
@@ -0,0 +1,289 @@
+{
+ "kind": "Pod",
+ "apiVersion": "v1",
+ "metadata": {
+ "name": "my-deployment-8d7c572790-46mgg",
+ "generateName": "my-deployment-8d7c572790-",
+ "namespace": "default",
+ "uid": "e393dfee-0ac3-4dd9-a10d-e748c0e3589a",
+ "resourceVersion": "14782",
+ "creationTimestamp": "2024-05-23T09:30:45Z",
+ "labels": {
+ "app": "my-app",
+ "pod-template-hash": "8d7c572790"
+ },
+ "ownerReferences": [
+ {
+ "apiVersion": "apps/v1",
+ "kind": "ReplicaSet",
+ "name": "my-deployment-8d7c572790",
+ "uid": "4aebb45f-c8b0-4406-8b81-3cfc9619d147",
+ "controller": true,
+ "blockOwnerDeletion": true
+ }
+ ],
+ "managedFields": [
+ {
+ "manager": "kube-controller-manager",
+ "operation": "Update",
+ "apiVersion": "v1",
+ "time": "2024-05-23T09:30:45Z",
+ "fieldsType": "FieldsV1",
+ "fieldsV1": {
+ "f:metadata": {
+ "f:generateName": {},
+ "f:labels": {
+ ".": {},
+ "f:app": {},
+ "f:pod-template-hash": {}
+ },
+ "f:ownerReferences": {
+ ".": {},
+ "k:{\"uid\":\"4aebb45f-c8b0-4406-8b81-3cfc9619d147\"}": {}
+ }
+ },
+ "f:spec": {
+ "f:containers": {
+ "k:{\"name\":\"my-container\"}": {
+ ".": {},
+ "f:env": {
+ ".": {},
+ "k:{\"name\":\"KUBERNETES_CONTAINER_NAME\"}": {
+ ".": {},
+ "f:name": {},
+ "f:value": {}
+ }
+ },
+ "f:image": {},
+ "f:imagePullPolicy": {},
+ "f:name": {},
+ "f:resources": {},
+ "f:terminationMessagePath": {},
+ "f:terminationMessagePolicy": {}
+ }
+ },
+ "f:dnsPolicy": {},
+ "f:enableServiceLinks": {},
+ "f:restartPolicy": {},
+ "f:schedulerName": {},
+ "f:securityContext": {},
+ "f:serviceAccount": {},
+ "f:serviceAccountName": {},
+ "f:terminationGracePeriodSeconds": {}
+ }
+ }
+ },
+ {
+ "manager": "kubelet",
+ "operation": "Update",
+ "apiVersion": "v1",
+ "time": "2024-05-23T09:30:47Z",
+ "fieldsType": "FieldsV1",
+ "fieldsV1": {
+ "f:status": {
+ "f:conditions": {
+ "k:{\"type\":\"ContainersReady\"}": {
+ ".": {},
+ "f:lastProbeTime": {},
+ "f:lastTransitionTime": {},
+ "f:status": {},
+ "f:type": {}
+ },
+ "k:{\"type\":\"Initialized\"}": {
+ ".": {},
+ "f:lastProbeTime": {},
+ "f:lastTransitionTime": {},
+ "f:status": {},
+ "f:type": {}
+ },
+ "k:{\"type\":\"PodReadyToStartContainers\"}": {
+ ".": {},
+ "f:lastProbeTime": {},
+ "f:lastTransitionTime": {},
+ "f:status": {},
+ "f:type": {}
+ },
+ "k:{\"type\":\"Ready\"}": {
+ ".": {},
+ "f:lastProbeTime": {},
+ "f:lastTransitionTime": {},
+ "f:status": {},
+ "f:type": {}
+ }
+ },
+ "f:containerStatuses": {},
+ "f:hostIP": {},
+ "f:hostIPs": {},
+ "f:phase": {},
+ "f:podIP": {},
+ "f:podIPs": {
+ ".": {},
+ "k:{\"ip\":\"10.244.0.11\"}": {
+ ".": {},
+ "f:ip": {}
+ }
+ },
+ "f:startTime": {}
+ }
+ },
+ "subresource": "status"
+ }
+ ]
+ },
+ "spec": {
+ "volumes": [
+ {
+ "name": "kube-api-access-npqxx",
+ "projected": {
+ "sources": [
+ {
+ "serviceAccountToken": {
+ "expirationSeconds": 3607,
+ "path": "token"
+ }
+ },
+ {
+ "configMap": {
+ "name": "kube-root-ca.crt",
+ "items": [
+ {
+ "key": "ca.crt",
+ "path": "ca.crt"
+ }
+ ]
+ }
+ },
+ {
+ "downwardAPI": {
+ "items": [
+ {
+ "path": "namespace",
+ "fieldRef": {
+ "apiVersion": "v1",
+ "fieldPath": "metadata.namespace"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "defaultMode": 420
+ }
+ }
+ ],
+ "containers": [
+ {
+ "name": "my-container",
+ "image": "app:latest",
+ "env": [
+ {
+ "name": "KUBERNETES_CONTAINER_NAME",
+ "value": "my-container"
+ }
+ ],
+ "resources": {},
+ "volumeMounts": [
+ {
+ "name": "kube-api-access-npqxx",
+ "readOnly": true,
+ "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+ }
+ ],
+ "terminationMessagePath": "/dev/termination-log",
+ "terminationMessagePolicy": "File",
+ "imagePullPolicy": "Never"
+ }
+ ],
+ "restartPolicy": "Always",
+ "terminationGracePeriodSeconds": 30,
+ "dnsPolicy": "ClusterFirst",
+ "serviceAccountName": "my-service-account",
+ "serviceAccount": "my-service-account",
+ "nodeName": "minikube",
+ "securityContext": {},
+ "schedulerName": "default-scheduler",
+ "tolerations": [
+ {
+ "key": "node.kubernetes.io/not-ready",
+ "operator": "Exists",
+ "effect": "NoExecute",
+ "tolerationSeconds": 300
+ },
+ {
+ "key": "node.kubernetes.io/unreachable",
+ "operator": "Exists",
+ "effect": "NoExecute",
+ "tolerationSeconds": 300
+ }
+ ],
+ "priority": 0,
+ "enableServiceLinks": true,
+ "preemptionPolicy": "PreemptLowerPriority"
+ },
+ "status": {
+ "phase": "Running",
+ "conditions": [
+ {
+ "type": "PodReadyToStartContainers",
+ "status": "True",
+ "lastProbeTime": null,
+ "lastTransitionTime": "2024-05-23T09:30:47Z"
+ },
+ {
+ "type": "Initialized",
+ "status": "True",
+ "lastProbeTime": null,
+ "lastTransitionTime": "2024-05-23T09:30:45Z"
+ },
+ {
+ "type": "Ready",
+ "status": "True",
+ "lastProbeTime": null,
+ "lastTransitionTime": "2024-05-23T09:30:47Z"
+ },
+ {
+ "type": "ContainersReady",
+ "status": "True",
+ "lastProbeTime": null,
+ "lastTransitionTime": "2024-05-23T09:30:47Z"
+ },
+ {
+ "type": "PodScheduled",
+ "status": "True",
+ "lastProbeTime": null,
+ "lastTransitionTime": "2024-05-23T09:30:45Z"
+ }
+ ],
+ "hostIP": "192.168.49.1",
+ "hostIPs": [
+ {
+ "ip": "192.168.49.1"
+ }
+ ],
+ "podIP": "10.244.0.11",
+ "podIPs": [
+ {
+ "ip": "10.244.0.11"
+ }
+ ],
+ "startTime": "2024-05-23T09:30:45Z",
+ "containerStatuses": [
+ {
+ "name": "my-container",
+ "state": {
+ "running": {
+ "startedAt": "2024-05-23T09:30:46Z"
+ }
+ },
+ "lastState": {},
+ "ready": true,
+ "restartCount": 0,
+ "image": "app:latest",
+ "imageID": "docker://sha256:7f8ec1d205a9364bf760b398cd21ae547ea08fd6213b594c64da80735bcf1e29",
+ "containerID": "docker://96724c05fa1be8d313f6db0e9872ca542b076839c4fd51ea4912a670ef538cbd",
+ "started": true
+ }
+ ],
+ "qosClass": "BestEffort"
+ }
+}