Skip to content

Commit

Permalink
Add Kubernetes support in Container Resource Detector
Browse files Browse the repository at this point in the history
  • Loading branch information
joegoldman2 committed Apr 27, 2024
1 parent 9d4b5f1 commit df0e7a4
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 60 deletions.
1 change: 1 addition & 0 deletions opentelemetry-dotnet-contrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
<Compile Include="$(RepoRoot)\src\Shared\IServerCertificateValidationEventSource.cs" Link="Includes\IServerCertificateValidationEventSource.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ResourceDetectorUtils.cs" Link="Includes\ResourceDetectorUtils.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ServerCertificateValidationHandler.cs" Link="Includes\ServerCertificateValidationHandler.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ServerCertificateValidationProvider.cs" Link="Includes\ServerCertificateValidationProvider.cs" />
</ItemGroup>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using OpenTelemetry.ResourceDetectors.Container.Models;
using OpenTelemetry.ResourceDetectors.Container.Utils;
using OpenTelemetry.Resources;

Expand All @@ -18,6 +21,13 @@ public class ContainerResourceDetector : IResourceDetector
private const string Filepath = "/proc/self/cgroup";
private const string FilepathV2 = "/proc/self/mountinfo";
private const string Hostname = "hostname";
private const string K8sServiceHostKey = "KUBERNETES_SERVICE_HOST";
private const string K8sServicePortKey = "KUBERNETES_SERVICE_PORT_HTTPS";
private const string K8sNamespaceKey = "KUBERNETES_NAMESPACE";
private const string K8sHostnameKey = "HOSTNAME";
private const string K8sContainerNameKey = "KUBERNETES_CONTAINER_NAME";
private const string K8sCertificatePath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt";
private const string K8sCredentialPath = "/var/run/secrets/kubernetes.io/serviceaccount/token";

/// <summary>
/// CGroup Parse Versions.
Expand All @@ -33,6 +43,11 @@ internal enum ParseMode
/// Represents CGroupV2.
/// </summary>
V2,

/// <summary>
/// Represents Kubernetes.
/// </summary>
K8s,
}

/// <summary>
Expand All @@ -41,24 +56,29 @@ internal enum ParseMode
/// <returns>Resource with key-value pairs of resource attributes.</returns>
public Resource Detect()
{
var cGroupBuild = this.BuildResource(Filepath, ParseMode.V1);
if (cGroupBuild == Resource.Empty)
var resource = this.BuildResource(Filepath, ParseMode.K8s);
if (resource == Resource.Empty)
{
cGroupBuild = this.BuildResource(FilepathV2, ParseMode.V2);
resource = this.BuildResource(Filepath, ParseMode.V1);
}

return cGroupBuild;
if (resource == Resource.Empty)
{
resource = this.BuildResource(FilepathV2, ParseMode.V2);
}

return resource;
}

/// <summary>
/// Builds the resource attributes from Container Id in file path.
/// </summary>
/// <param name="path">File path where container id exists.</param>
/// <param name="cgroupVersion">CGroup Version of file to parse from.</param>
/// <param name="parseMode">CGroup Version of file to parse from.</param>
/// <returns>Returns Resource with list of key-value pairs of container resource attributes if container id exists else empty resource.</returns>
internal Resource BuildResource(string path, ParseMode cgroupVersion)
internal Resource BuildResource(string path, ParseMode parseMode)
{
var containerId = this.ExtractContainerId(path, cgroupVersion);
var containerId = this.ExtractContainerId(path, parseMode);

if (string.IsNullOrEmpty(containerId))
{
Expand Down Expand Up @@ -132,45 +152,125 @@ private static string RemovePrefixAndSuffixIfNeeded(string input, int startIndex
return input.Substring(startIndex, endIndex - startIndex);
}

private static string? ExtractK8sContainerId()
{
try
{
var host = Environment.GetEnvironmentVariable(K8sServiceHostKey);
var port = Environment.GetEnvironmentVariable(K8sServicePortKey);
var @namespace = Environment.GetEnvironmentVariable(K8sNamespaceKey);
var hostname = Environment.GetEnvironmentVariable(K8sHostnameKey);
var containerName = Environment.GetEnvironmentVariable(K8sContainerNameKey);
var url = $"https://{host}:{port}/api/v1/namespaces/{@namespace}/pods/{hostname}";
var credentials = GetK8sCredentials(K8sCredentialPath);
using var httpClientHandler = ServerCertificateValidationHandler.Create(K8sCertificatePath, ContainerResourceEventSource.Log);
var response = ResourceDetectorUtils.SendOutRequest(url, "GET", new KeyValuePair<string, string>("Authorization", credentials), httpClientHandler).GetAwaiter().GetResult();

Check failure on line 167 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceDetector.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (windows-latest, net6.0)

Possible null reference argument for parameter 'value' in 'KeyValuePair<string, string>.KeyValuePair(string key, string value)'.

Check failure on line 167 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceDetector.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (windows-latest, net7.0)

Possible null reference argument for parameter 'value' in 'KeyValuePair<string, string>.KeyValuePair(string key, string value)'.

Check failure on line 167 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceDetector.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (windows-latest, net8.0)

Possible null reference argument for parameter 'value' in 'KeyValuePair<string, string>.KeyValuePair(string key, string value)'.

Check failure on line 167 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceDetector.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (ubuntu-latest, net6.0)

Possible null reference argument for parameter 'value' in 'KeyValuePair<string, string>.KeyValuePair(string key, string value)'.

Check failure on line 167 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceDetector.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (ubuntu-latest, net7.0)

Possible null reference argument for parameter 'value' in 'KeyValuePair<string, string>.KeyValuePair(string key, string value)'.

Check failure on line 167 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceDetector.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (ubuntu-latest, net8.0)

Possible null reference argument for parameter 'value' in 'KeyValuePair<string, string>.KeyValuePair(string key, string value)'.
var pod = DeserializeK8sResponse(response);

if (pod == null || pod.Status == null || pod.Status.ContainerStatuses == null)
{
return string.Empty;
}

var container = pod.Status.ContainerStatuses.SingleOrDefault(p => p.Name == containerName);
if (container is null)
{
return string.Empty;
}

// Container's ID is in <type>://<container_id> format.
var index = container.Id.LastIndexOf('/');
return container.Id.Substring(index + 1);
}
catch (Exception ex)
{
ContainerResourceEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerResourceDetector)}: Failed to extract container id", ex);
}

return null;

static string? GetK8sCredentials(string path)
{
try
{
var stringBuilder = new StringBuilder();

using (var streamReader = ResourceDetectorUtils.GetStreamReader(path))
{
while (!streamReader.EndOfStream)
{
stringBuilder.Append(streamReader.ReadLine()?.Trim());
}
}

stringBuilder.Insert(0, "Bearer ");

return stringBuilder.ToString();
}
catch (Exception ex)
{
ContainerResourceEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerResourceDetector)}: Failed to load client token", ex);
}

return null;
}

static K8sPod? DeserializeK8sResponse(string response)
{
#if NET6_0_OR_GREATER
return ResourceDetectorUtils.DeserializeFromString(response, SourceGenerationContext.Default.K8sPod);
#else
return ResourceDetectorUtils.DeserializeFromString<K8sPod>(response);
#endif
}
}

/// <summary>
/// Extracts Container Id from path using the cgroupv1 format.
/// </summary>
/// <param name="path">cgroup path.</param>
/// <param name="cgroupVersion">CGroup Version of file to parse from.</param>
/// <returns>Container Id, Null if not found or exception being thrown.</returns>
private string? ExtractContainerId(string path, ParseMode cgroupVersion)
/// <param name="parseMode">CGroup Version of file to parse from.</param>
/// <returns>Container Id, <see langword="null" /> if not found or exception being thrown.</returns>
private string? ExtractContainerId(string path, ParseMode parseMode)
{
try
{
if (!File.Exists(path))
if (parseMode == ParseMode.K8s)
{
return null;
return ExtractK8sContainerId();
}

foreach (string line in File.ReadLines(path))
else
{
string? containerId = null;
if (!string.IsNullOrEmpty(line))
if (!File.Exists(path))
{
return null;
}

foreach (string line in File.ReadLines(path))
{
if (cgroupVersion == ParseMode.V1)
string? containerId = null;
if (!string.IsNullOrEmpty(line))
{
containerId = GetIdFromLineV1(line);
if (parseMode == ParseMode.V1)
{
containerId = GetIdFromLineV1(line);
}
else if (parseMode == ParseMode.V2 && line.Contains(Hostname, StringComparison.Ordinal))
{
containerId = GetIdFromLineV2(line);
}
}
else if (cgroupVersion == ParseMode.V2 && line.Contains(Hostname, StringComparison.Ordinal))

if (!string.IsNullOrEmpty(containerId))
{
containerId = GetIdFromLineV2(line);
return containerId;
}
}

if (!string.IsNullOrEmpty(containerId))
{
return containerId;
}
}
}
catch (Exception ex)
{
ContainerExtensionsEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerResourceDetector)} : Failed to extract Container id from path", ex);
ContainerResourceEventSource.Log.ExtractResourceAttributesException($"{nameof(ContainerResourceDetector)} : Failed to extract Container id from path", ex);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Diagnostics.Tracing;
using OpenTelemetry.Internal;

namespace OpenTelemetry.ResourceDetectors.Container;

[EventSource(Name = "OpenTelemetry-ResourceDetectors-Container")]
internal class ContainerResourceEventSource : EventSource, IServerCertificateValidationEventSource
{
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;

public static ContainerResourceEventSource Log = new();

Check failure on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (windows-latest, net6.0)

Check failure on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (windows-latest, net7.0)

Check failure on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (windows-latest, net8.0)

Check failure on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (ubuntu-latest, net6.0)

Check failure on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (ubuntu-latest, net7.0)

Check failure on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / build-test-solution (ubuntu-latest, net8.0)

Check warning on line 19 in src/OpenTelemetry.ResourceDetectors.Container/ContainerResourceEventSource.cs

View workflow job for this annotation

GitHub Actions / lint-dotnet-format / run-dotnet-format

'public' members should come before 'private' members

[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(1, 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json.Serialization;

namespace OpenTelemetry.ResourceDetectors.Container.Models;

internal sealed class K8sContainerStatus
{
[JsonPropertyName("name")]
public string Name { get; set; } = default!;

[JsonPropertyName("containerID")]
public string Id { get; set; } = default!;
}
12 changes: 12 additions & 0 deletions src/OpenTelemetry.ResourceDetectors.Container/Models/K8sPod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Text.Json.Serialization;

namespace OpenTelemetry.ResourceDetectors.Container.Models;

internal sealed class K8sPod
{
[JsonPropertyName("status")]
public K8sPodStatus? Status { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace OpenTelemetry.ResourceDetectors.Container.Models;

internal sealed class K8sPodStatus
{
[JsonPropertyName("containerStatuses")]
public IReadOnlyList<K8sContainerStatus> ContainerStatuses { get; set; } = new List<K8sContainerStatus>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@

<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\IServerCertificateValidationEventSource.cs" Link="Includes\IServerCertificateValidationEventSource.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ResourceDetectorUtils.cs" Link="Includes\ResourceDetectorUtils.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ServerCertificateValidationHandler.cs" Link="Includes\ServerCertificateValidationHandler.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ServerCertificateValidationProvider.cs" Link="Includes\ServerCertificateValidationProvider.cs" />
</ItemGroup>
</Project>
Loading

0 comments on commit df0e7a4

Please sign in to comment.