Repository: open-telemetry/opentelemetry-php (php)

Issue:
Resource Detection for container properties
**Is your feature request related to a problem?**
Detecting the id of the container that holds the service instrumented with otel allows end-users to correlate container issues with service issues.

**Describe the solution you'd like**
A resource detector that can be added to the SDK and then does the reading of container.id for me automatically.

**Describe alternatives you've considered**
Injecting the container id via an environment variable is always possible.

**Additional context**

Similar implementations exist for Go, JS, Java, .NET and the logic can be reused for a PHP implementation as well:

* https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/detectors/node/opentelemetry-resource-detector-container/src/detectors/ContainerDetector.ts
* https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/main/src/OpenTelemetry.Extensions.Docker/Resources/DockerResourceDetector.cs
* https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/resource/container.go
* https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/resources/library/src/main/java/io/opentelemetry/instrumentation/resources/ContainerResourceProvider.java


Pull request description:
adding container detector
Detect container id from cgroup or mountinfo, based on how otel-js does it.

Closes #931 

Patch:
diff --git a/src/SDK/Common/Configuration/KnownValues.php b/src/SDK/Common/Configuration/KnownValues.php
index b10651e4..7f885bab 100644
--- a/src/SDK/Common/Configuration/KnownValues.php
+++ b/src/SDK/Common/Configuration/KnownValues.php
@@ -165,6 +165,7 @@ interface KnownValues
     public const VALUE_DETECTORS_PROCESS_RUNTIME = 'process_runtime';
     public const VALUE_DETECTORS_SDK = 'sdk';
     public const VALUE_DETECTORS_SDK_PROVIDED = 'sdk_provided';
+    public const VALUE_DETECTORS_CONTAINER = 'container';
     public const OTEL_PHP_DETECTORS = [
         self::VALUE_ALL,
         self::VALUE_DETECTORS_ENVIRONMENT,
@@ -174,6 +175,7 @@ interface KnownValues
         self::VALUE_DETECTORS_PROCESS_RUNTIME,
         self::VALUE_DETECTORS_SDK,
         self::VALUE_DETECTORS_SDK_PROVIDED,
+        self::VALUE_DETECTORS_CONTAINER,
         self::VALUE_NONE,
     ];
 }
diff --git a/src/SDK/Resource/Detectors/Container.php b/src/SDK/Resource/Detectors/Container.php
new file mode 100644
index 00000000..0e763f44
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Container.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Common\Attribute\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+
+/**
+ * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.18.0/specification/resource/semantic_conventions/container.md
+ */
+final class Container implements ResourceDetectorInterface
+{
+    private string $dir;
+    private const CONTAINER_ID_LENGTH = 64;
+    private const CGROUP_V1 = 'cgroup';
+    private const CGROUP_V2 = 'mountinfo';
+    private const HOSTNAME = 'hostname';
+
+    public function __construct(string $dir = '/proc/self')
+    {
+        $this->dir = $dir;
+    }
+
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [];
+        $id = $this->getContainerId();
+        if ($id) {
+            $attributes[ResourceAttributes::CONTAINER_ID] = $id;
+        }
+
+        return ResourceInfo::create(Attributes::create($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+
+    private function getContainerId(): ?string
+    {
+        return $this->getContainerIdV2() ?? $this->getContainerIdV1();
+    }
+
+    private function getContainerIdV1(): ?string
+    {
+        $data = file_get_contents(sprintf('%s/%s', $this->dir, self::CGROUP_V1));
+        if (!$data) {
+            return null;
+        }
+        $lines = explode('\n', $data);
+        foreach ($lines as $line) {
+            if (strlen($line) >= self::CONTAINER_ID_LENGTH) {
+                //if string is longer than CONTAINER_ID_LENGTH, return the last CONTAINER_ID_LENGTH chars
+                return substr($line, strlen($line) - self::CONTAINER_ID_LENGTH);
+            }
+        }
+
+        return null;
+    }
+    private function getContainerIdV2(): ?string
+    {
+        $data = file_get_contents(sprintf('%s/%s', $this->dir, self::CGROUP_V2));
+        if (!$data) {
+            return null;
+        }
+        $lines = explode(PHP_EOL, $data);
+        foreach ($lines as $line) {
+            if (strpos($line, self::HOSTNAME) !== false) {
+                $parts = explode('/', $line);
+                foreach ($parts as $part) {
+                    if (strlen($part) === self::CONTAINER_ID_LENGTH) {
+                        return $part;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/src/SDK/Resource/ResourceInfoFactory.php b/src/SDK/Resource/ResourceInfoFactory.php
index bb9e6b51..1c19664a 100644
--- a/src/SDK/Resource/ResourceInfoFactory.php
+++ b/src/SDK/Resource/ResourceInfoFactory.php
@@ -44,6 +44,7 @@ class ResourceInfoFactory
                 new Detectors\ProcessRuntime(),
                 new Detectors\Sdk(),
                 new Detectors\SdkProvided(),
+                new Detectors\Container(),
             ]))->getResource();
         }
 
@@ -78,6 +79,10 @@ class ResourceInfoFactory
                 case Values::VALUE_DETECTORS_SDK_PROVIDED:
                     $resourceDetectors[] = new Detectors\SdkProvided();
 
+                    break;
+                case Values::VALUE_DETECTORS_CONTAINER:
+                    $resourceDetectors[] = new Detectors\Container();
+
                     break;
                 default:
             }


Test patch:
diff --git a/tests/Unit/SDK/Resource/Detectors/ContainerTest.php b/tests/Unit/SDK/Resource/Detectors/ContainerTest.php
new file mode 100644
index 00000000..23ea2202
--- /dev/null
+++ b/tests/Unit/SDK/Resource/Detectors/ContainerTest.php
@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\Example\Unit\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Resource\Detectors\Container;
+use OpenTelemetry\SemConv\ResourceAttributes;
+use org\bovigo\vfs\vfsStream;
+use org\bovigo\vfs\vfsStreamFile;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers OpenTelemetry\SDK\Resource\Detectors\Container
+ */
+class ContainerTest extends TestCase
+{
+    private vfsStreamFile $cgroup;
+    private vfsStreamFile $mountinfo;
+    private Container $detector;
+
+    public function setUp(): void
+    {
+        $root = vfsStream::setup();
+        $this->cgroup = vfsStream::newFile('cgroup')->at($root);
+        $this->mountinfo = vfsStream::newFile('mountinfo')->at($root);
+        $this->detector = new Container($root->url());
+    }
+
+    public function test_valid_v1(): void
+    {
+        $valid = 'a8493b8a4f6f23b65c5db50be86619ca4da078da040aa3d5ccff26fe50de205d';
+        $this->cgroup->setContent($valid);
+        $resource = $this->detector->getResource();
+
+        $this->assertSame(ResourceAttributes::SCHEMA_URL, $resource->getSchemaUrl());
+        $this->assertIsString($resource->getAttributes()->get(ResourceAttributes::CONTAINER_ID));
+        $this->assertSame($valid, $resource->getAttributes()->get(ResourceAttributes::CONTAINER_ID));
+    }
+
+    public function test_invalid_v1(): void
+    {
+        $this->cgroup->setContent('0::/');
+        $resource = $this->detector->getResource();
+
+        $this->assertEmpty($resource->getAttributes());
+    }
+
+    public function test_valid_v2(): void
+    {
+        $expected = 'a8493b8a4f6f23b65c5db50be86619ca4da078da040aa3d5ccff26fe50de205d';
+        $data = <<< EOS
+1366 1365 0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw
+1408 1362 0:107 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+1579 1362 0:112 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64
+1581 1359 259:2 /var/lib/docker/containers/a8493b8a4f6f23b65c5db50be86619ca4da078da040aa3d5ccff26fe50de205d/hostname /etc/hostname rw,relatime - ext4 /dev/nvme0n1p2 rw,errors=remount-ro
+1583 1359 259:3 /brett/docker/otel/opentelemetry-php /usr/src/myapp rw,relatime - ext4 /dev/nvme0n1p3 rw
+EOS;
+        $this->mountinfo->withContent($data);
+        $resource = $this->detector->getResource();
+
+        $this->assertCount(1, $resource->getAttributes());
+        $this->assertSame($expected, $resource->getAttributes()->get(ResourceAttributes::CONTAINER_ID));
+    }
+
+    public function test_invalid_v2(): void
+    {
+        $data = <<< EOS
+1581 1359 259:2 /var/lib/docker/containers/a8493b8a4f6f23b65c5db50be86619ca4da078da040aa3d5ccff26fe50de205d/wrongkeyword
+EOS;
+        $this->mountinfo->withContent($data);
+        $resource = $this->detector->getResource();
+
+        $this->assertEmpty($resource->getAttributes());
+    }
+}