-
Notifications
You must be signed in to change notification settings - Fork 269
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
df0e7a4
7a966f3
c7a08b0
0beaa51
966b860
fde4504
2af92bc
1a3b5e6
9139514
c1fe35f
c59199c
ac24d7f
803528e
d5d67d4
310975b
d609b39
60f2137
e66dde2
a96d991
6f810af
d6521c3
2977bc7
63918f1
a263fc4
48b728a
fdd3d27
056b4b2
81d068b
be15a32
f5c4122
811ba2b
f093b0c
7cdcdc9
09d1785
5442c3f
4dd428d
2f1ab7e
72463c5
0c3bad3
5a4c937
fed4283
1e9ecbf
2ba517e
740efcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
||
|
@@ -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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Namespace should be published in |
||
private const string K8sHostnameKey = "HOSTNAME"; | ||
private const string K8sContainerNameKey = "KUBERNETES_CONTAINER_NAME"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's the same for
I'm not sure that's the purpose of this detector. From my point of view, the user can already use the env variable |
||
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. | ||
|
@@ -33,6 +43,11 @@ internal enum ParseMode | |
/// Represents CGroupV2. | ||
/// </summary> | ||
V2, | ||
|
||
/// <summary> | ||
/// Represents Kubernetes. | ||
/// </summary> | ||
K8s, | ||
} | ||
|
||
/// <summary> | ||
|
@@ -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)) | ||
{ | ||
|
@@ -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}"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the guarantee that this will be a well-formed URL? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
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!; | ||
} |
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>(); | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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
andKUBERNETES_CONTAINER_NAME
are not available by default in Kubernetes. The user will have to provide them using the downward API.