Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Resources.Container] Add Kubernetes support #1699

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
df0e7a4
Add Kubernetes support in Container Resource Detector
joegoldman2 Apr 27, 2024
7a966f3
Update CHANGELOG.md
joegoldman2 Apr 27, 2024
c7a08b0
Fix warnings
joegoldman2 Apr 27, 2024
0beaa51
Return string.Empty if container id is null or empty
joegoldman2 Apr 27, 2024
966b860
Update CHANGELOG.md
joegoldman2 Apr 27, 2024
fde4504
Remove blank line
joegoldman2 Apr 27, 2024
2af92bc
Apply suggestions from review
joegoldman2 Apr 30, 2024
1a3b5e6
Merge branch 'main' into fix/1562
joegoldman2 May 7, 2024
9139514
Merge branch 'main' into fix/1562
cijothomas May 17, 2024
c1fe35f
Apply suggestions from review
joegoldman2 May 23, 2024
c59199c
Update README.md
joegoldman2 May 23, 2024
ac24d7f
Update README.md
joegoldman2 May 23, 2024
803528e
Merge branch 'main' into fix/1562
joegoldman2 May 23, 2024
d5d67d4
Update README.md
joegoldman2 May 23, 2024
310975b
Add unit test
joegoldman2 May 23, 2024
d609b39
Add null check
joegoldman2 May 23, 2024
60f2137
Update README
joegoldman2 May 25, 2024
e66dde2
Fix README lint errors
joegoldman2 May 25, 2024
a96d991
Fast fail for non K8s environment
joegoldman2 May 25, 2024
6f810af
Remove extra line
joegoldman2 May 25, 2024
d6521c3
Merge branch 'main' into fix/1562
joegoldman2 May 25, 2024
2977bc7
Fast fail for non K8s env and container name not provided
joegoldman2 May 25, 2024
63918f1
Merge branch 'main' into fix/1562
joegoldman2 May 27, 2024
a263fc4
Merge branch 'main' into fix/1562
joegoldman2 May 29, 2024
48b728a
Merge branch 'main' into fix/1562
joegoldman2 May 30, 2024
fdd3d27
Merge branch 'main' into fix/1562
joegoldman2 May 30, 2024
056b4b2
Merge branch 'main' into fix/1562
joegoldman2 Jun 4, 2024
81d068b
Merge branch 'main' into fix/1562
joegoldman2 Jun 4, 2024
be15a32
Fix EventSource name
joegoldman2 Jun 4, 2024
f5c4122
Fix CHANGELOG
joegoldman2 Jun 4, 2024
811ba2b
Apply suggestions from review
joegoldman2 Jun 4, 2024
f093b0c
Merge branch 'main' into fix/1562
joegoldman2 Jun 4, 2024
7cdcdc9
Merge branch 'main' into fix/1562
joegoldman2 Jun 6, 2024
09d1785
Apply suggestions from review
joegoldman2 Jun 6, 2024
5442c3f
Simplify deserialization
joegoldman2 Jun 6, 2024
4dd428d
Remove unnecessary using
joegoldman2 Jun 6, 2024
2f1ab7e
Merge branch 'main' into fix/1562
joegoldman2 Jun 28, 2024
72463c5
Remove unnecessary usings
joegoldman2 Jun 28, 2024
0c3bad3
Move and use AsyncHelper
joegoldman2 Jun 28, 2024
5a4c937
Merge branch 'main' into fix/1562
joegoldman2 Jul 23, 2024
fed4283
Update src/OpenTelemetry.Resources.Container/ContainerResourceEventSo…
joegoldman2 Aug 6, 2024
1e9ecbf
Merge branch 'main' into fix/1562
joegoldman2 Aug 6, 2024
2ba517e
Merge branch 'main' into fix/1562
joegoldman2 Aug 6, 2024
740efcb
Merge branch 'main' into fix/1562
joegoldman2 Aug 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -67,7 +67,7 @@ internal static List<KeyValuePair<string, object>> ExtractResourceAttributes(str
{
try
{
var stringBuilder = new StringBuilder();
var stringBuilder = new StringBuilder("Bearer ");

using (var streamReader = ResourceDetectorUtils.GetStreamReader(path))
{
Expand All @@ -77,8 +77,6 @@ internal static List<KeyValuePair<string, object>> ExtractResourceAttributes(str
}
}

stringBuilder.Insert(0, "Bearer ");

return stringBuilder.ToString();
}
catch (Exception ex)
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
3 changes: 3 additions & 0 deletions src/OpenTelemetry.ResourceDetectors.Container/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Update OpenTelemetry SDK version to `1.8.1`.
([#1668](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1668))

* Add Kubernetes support.
([#1699](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1699))

## 1.0.0-beta.7

Released 2024-Apr-05
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";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this environment variable and KUBERNETES_CONTAINER_NAME available in all Kubernetes environments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I know, KUBERNETES_NAMESPACE and KUBERNETES_CONTAINER_NAME are not available by default in Kubernetes. The user will have to provide them using the downward API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Namespace should be published in /var/run/secrets/kubernetes.io/serviceaccount/namespace. Probably the best way to read it from file instead of asking user to configure passing it in environment variable though downwards api.

private const string K8sHostnameKey = "HOSTNAME";
private const string K8sContainerNameKey = "KUBERNETES_CONTAINER_NAME";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tricky part here - k8s doesn't have any standard variable to provide container name. It is entirely based on the user. I don't see any issues that extractor would expect it to be named "KUBERNETES_CONTAINER_NAME" - but probably it may be better to be passed by user to detector as constructor argument.
If it is passed by user, we can also report it as additional attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's the same for KUBERNETES_NAMESPACE in this case, which is not standard as well. Perhaps we can let the user supply the values via the constructor but also keep automatic detection via the environment variables if they are not supplied. What do you think?

If it is passed by user, we can also report it as additional attribute.

I'm not sure that's the purpose of this detector. From my point of view, the user can already use the env variable OTEL_RESOURCE_ATTRIBUTES or implement his own detector to add this resource attribute.

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,127 @@ 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}";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the guarantee that this will be a well-formed URL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing I think but in this case an exception will be thrown, catched and logged using the EventSource.

var credentials = GetK8sCredentials(K8sCredentialPath);
if (string.IsNullOrEmpty(credentials))
{
return string.Empty;
}

using var httpClientHandler = ServerCertificateValidationHandler.Create(K8sCertificatePath, ContainerResourceEventSource.Log);
var response = ResourceDetectorUtils.SendOutRequest(url, "GET", new KeyValuePair<string, string>("Authorization", credentials), httpClientHandler).GetAwaiter().GetResult();
joegoldman2 marked this conversation as resolved.
Show resolved Hide resolved
var pod = DeserializeK8sResponse(response);
if (pod?.Status?.ContainerStatuses == null)
{
return string.Empty;
}

var container = pod.Status.ContainerStatuses.SingleOrDefault(p => p.Name == containerName);
if (container is null || string.IsNullOrEmpty(container.Id))
{
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("Bearer ");

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

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))
{
if (cgroupVersion == ParseMode.V1)
return null;
}

foreach (string line in File.ReadLines(path))
{
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
{
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(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>();
}
Loading
Loading