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

Issue:
`ResourceInfo` needs a dedicated factory
I believe `ResoerceInfo` needs a dedicated factory. It's overloaded with factory methods, while creating objects is not its main responsibility. However this could be changed in a later PR.

_Originally posted by @tidal in https://github.com/open-telemetry/opentelemetry-php/pull/469#r748851138_

Pull request description:
Move resource detection out of `ResourceInfo`
Moves resource detection out of `ResourceInfo` and adds some additional attributes.

`ResourceInfo::merge()` no longer differs between empty and non-empty values ([the value of the updating resource MUST be picked (even if the updated value is empty)](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/resource/sdk.md#merge)).
`ResourceInfo::create()` no longer adds the default resource attributes.

Fixes #473.

Patch:
diff --git a/.phan/config.php b/.phan/config.php
index 3f50e1f4..5cfe547f 100644
--- a/.phan/config.php
+++ b/.phan/config.php
@@ -362,7 +362,7 @@ return [
         'proto',
         'thrift',
         'vendor/packaged/thrift',
-        'vendor/composer/xdebug-handler/src',
+        'vendor/composer',
         'vendor/guzzlehttp',
         'vendor/psr',
         'vendor/php-http',
diff --git a/examples/SpanResources.php b/examples/SpanResources.php
index 220c8b4c..0f7c2415 100644
--- a/examples/SpanResources.php
+++ b/examples/SpanResources.php
@@ -5,24 +5,21 @@ require __DIR__ . '/../vendor/autoload.php';
 
 use OpenTelemetry\SDK\Attributes;
 use OpenTelemetry\SDK\Resource\ResourceInfo;
-use OpenTelemetry\SDK\Trace\SpanProcessor\ConsoleSpanExporter;
+use OpenTelemetry\SDK\Trace\SpanExporter\ConsoleSpanExporter;
 use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
 use OpenTelemetry\SDK\Trace\TracerProvider;
 use OpenTelemetry\SemConv\ResourceAttributes;
 
 echo 'Starting ConsoleSpanExporter' . PHP_EOL;
 
-$resource = ResourceInfo::create(new Attributes([
+$resource = ResourceInfo::merge(ResourceInfo::create(new Attributes([
     //@see https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions
     ResourceAttributes::SERVICE_NAMESPACE => 'foo',
     ResourceAttributes::SERVICE_NAME => 'bar',
     ResourceAttributes::SERVICE_INSTANCE_ID => 1,
     ResourceAttributes::SERVICE_VERSION => '0.1',
-    ResourceAttributes::HOST_NAME => \gethostname(),
     ResourceAttributes::DEPLOYMENT_ENVIRONMENT => 'development',
-    ResourceAttributes::HOST_ARCH => strtolower(php_uname('m')),
-    ResourceAttributes::HOST_TYPE => strtolower(php_uname('s')),
-]));
+])), ResourceInfo::defaultResource());
 
 $tracerProvider =  new TracerProvider(
     new SimpleSpanProcessor(
diff --git a/src/SDK/Resource/Detectors/Composer.php b/src/SDK/Resource/Detectors/Composer.php
new file mode 100644
index 00000000..b24e99fb
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Composer.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use function class_exists;
+use Composer\InstalledVersions;
+use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+
+final class Composer implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        if (!class_exists(InstalledVersions::class)) {
+            return ResourceInfo::emptyResource();
+        }
+
+        $attributes = [
+            ResourceAttributes::SERVICE_NAME => InstalledVersions::getRootPackage()['name'],
+            ResourceAttributes::SERVICE_VERSION => InstalledVersions::getRootPackage()['pretty_version'],
+        ];
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/Composite.php b/src/SDK/Resource/Detectors/Composite.php
new file mode 100644
index 00000000..2e699a17
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Composite.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+
+final class Composite implements ResourceDetectorInterface
+{
+    private iterable $resourceDetectors;
+
+    /**
+     * @param iterable<ResourceDetectorInterface> $resourceDetectors
+     */
+    public function __construct(iterable $resourceDetectors)
+    {
+        $this->resourceDetectors = $resourceDetectors;
+    }
+
+    public function getResource(): ResourceInfo
+    {
+        $resources = [];
+        foreach ($this->resourceDetectors as $resourceDetector) {
+            $resources[] = $resourceDetector->getResource();
+        }
+
+        return ResourceInfo::merge(...$resources);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/Constant.php b/src/SDK/Resource/Detectors/Constant.php
new file mode 100644
index 00000000..7ff9d19e
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Constant.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+
+final class Constant implements ResourceDetectorInterface
+{
+    private ResourceInfo $resourceInfo;
+
+    public function __construct(ResourceInfo $resourceInfo)
+    {
+        $this->resourceInfo = $resourceInfo;
+    }
+
+    public function getResource(): ResourceInfo
+    {
+        return $this->resourceInfo;
+    }
+}
diff --git a/src/SDK/Resource/Detectors/Environment.php b/src/SDK/Resource/Detectors/Environment.php
new file mode 100644
index 00000000..4b98044a
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Environment.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use function explode;
+use function getenv;
+use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+use function strpos;
+use function trim;
+
+/**
+ * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable
+ */
+final class Environment implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [];
+
+        $string = getenv('OTEL_RESOURCE_ATTRIBUTES');
+        if ($string && false !== strpos($string, '=')) {
+            foreach (explode(',', $string) as $pair) {
+                [$key, $value] = explode('=', $pair);
+                $attributes[trim($key)] = trim($value);
+            }
+        }
+        //@see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#general-sdk-configuration
+        $serviceName = getenv('OTEL_SERVICE_NAME');
+        if ($serviceName) {
+            $attributes[ResourceAttributes::SERVICE_NAME] = $serviceName;
+        }
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/Host.php b/src/SDK/Resource/Detectors/Host.php
new file mode 100644
index 00000000..c94c9185
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Host.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+use function php_uname;
+
+/**
+ * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/resource/semantic_conventions/host.md#host
+ */
+final class Host implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [
+            ResourceAttributes::HOST_NAME => php_uname('n'),
+            ResourceAttributes::HOST_ARCH => php_uname('m'),
+        ];
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/OperatingSystem.php b/src/SDK/Resource/Detectors/OperatingSystem.php
new file mode 100644
index 00000000..f26b8d30
--- /dev/null
+++ b/src/SDK/Resource/Detectors/OperatingSystem.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+use const PHP_OS;
+use const PHP_OS_FAMILY;
+use function php_uname;
+use function strtolower;
+
+/**
+ * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/resource/semantic_conventions/os.md
+ */
+final class OperatingSystem implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [
+            ResourceAttributes::OS_TYPE => strtolower(PHP_OS_FAMILY),
+            ResourceAttributes::OS_DESCRIPTION => php_uname('r'),
+            ResourceAttributes::OS_NAME => PHP_OS,
+            ResourceAttributes::OS_VERSION => php_uname('v'),
+        ];
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/Process.php b/src/SDK/Resource/Detectors/Process.php
new file mode 100644
index 00000000..0ae80e10
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Process.php
@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use function extension_loaded;
+use function getmypid;
+use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+use const PHP_BINARY;
+
+/**
+ * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/resource/semantic_conventions/process.md#process
+ */
+final class Process implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [];
+        $attributes[ResourceAttributes::PROCESS_PID] = getmypid();
+        $attributes[ResourceAttributes::PROCESS_EXECUTABLE_PATH] = PHP_BINARY;
+        if ($_SERVER['argv'] ?? null) {
+            $attributes[ResourceAttributes::PROCESS_COMMAND] = $_SERVER['argv'][0];
+            $attributes[ResourceAttributes::PROCESS_COMMAND_ARGS] = $_SERVER['argv'];
+        }
+
+        if (extension_loaded('posix')) {
+            $attributes[ResourceAttributes::PROCESS_OWNER] = \posix_getpwuid(\posix_geteuid())['name'];
+        }
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/ProcessRuntime.php b/src/SDK/Resource/Detectors/ProcessRuntime.php
new file mode 100644
index 00000000..446ded40
--- /dev/null
+++ b/src/SDK/Resource/Detectors/ProcessRuntime.php
@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\ResourceDetectorInterface;
+use OpenTelemetry\SDK\Resource\ResourceInfo;
+use OpenTelemetry\SemConv\ResourceAttributes;
+use function php_sapi_name;
+use const PHP_VERSION;
+
+/**
+ * @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.8.0/specification/resource/semantic_conventions/process.md#process-runtimes
+ */
+final class ProcessRuntime implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [
+            ResourceAttributes::PROCESS_RUNTIME_NAME => php_sapi_name(),
+            ResourceAttributes::PROCESS_RUNTIME_VERSION => PHP_VERSION,
+        ];
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/Sdk.php b/src/SDK/Resource/Detectors/Sdk.php
new file mode 100644
index 00000000..89890fb1
--- /dev/null
+++ b/src/SDK/Resource/Detectors/Sdk.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use function class_exists;
+use Composer\InstalledVersions;
+use OpenTelemetry\SDK\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.8.0/specification/resource/semantic_conventions/README.md#telemetry-sdk
+ */
+final class Sdk implements ResourceDetectorInterface
+{
+    private const PACKAGES = [
+        'open-telemetry/sdk',
+        'open-telemetry/opentelemetry',
+    ];
+
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [
+            ResourceAttributes::TELEMETRY_SDK_NAME => 'opentelemetry',
+            ResourceAttributes::TELEMETRY_SDK_LANGUAGE => 'php',
+        ];
+
+        if (class_exists(InstalledVersions::class)) {
+            foreach (self::PACKAGES as $package) {
+                if (!InstalledVersions::isInstalled($package)) {
+                    continue;
+                }
+                if (($version = InstalledVersions::getPrettyVersion($package)) === null) {
+                    continue;
+                }
+
+                $attributes[ResourceAttributes::TELEMETRY_SDK_VERSION] = $version;
+
+                break;
+            }
+        }
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/Detectors/SdkProvided.php b/src/SDK/Resource/Detectors/SdkProvided.php
new file mode 100644
index 00000000..6abddacd
--- /dev/null
+++ b/src/SDK/Resource/Detectors/SdkProvided.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource\Detectors;
+
+use OpenTelemetry\SDK\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.8.0/specification/resource/semantic_conventions/README.md#semantic-attributes-with-sdk-provided-default-value
+ */
+final class SdkProvided implements ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo
+    {
+        $attributes = [
+            ResourceAttributes::SERVICE_NAME => 'unknown_service',
+        ];
+
+        return ResourceInfo::create(new Attributes($attributes), ResourceAttributes::SCHEMA_URL);
+    }
+}
diff --git a/src/SDK/Resource/ResourceDetectorInterface.php b/src/SDK/Resource/ResourceDetectorInterface.php
new file mode 100644
index 00000000..f2cd5256
--- /dev/null
+++ b/src/SDK/Resource/ResourceDetectorInterface.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace OpenTelemetry\SDK\Resource;
+
+interface ResourceDetectorInterface
+{
+    public function getResource(): ResourceInfo;
+}
diff --git a/src/SDK/Resource/ResourceInfo.php b/src/SDK/Resource/ResourceInfo.php
index ea03f28b..ab48e5da 100644
--- a/src/SDK/Resource/ResourceInfo.php
+++ b/src/SDK/Resource/ResourceInfo.php
@@ -6,7 +6,6 @@ namespace OpenTelemetry\SDK\Resource;
 
 use OpenTelemetry\SDK\Attributes;
 use OpenTelemetry\SDK\AttributesInterface;
-use OpenTelemetry\SemConv\ResourceAttributes;
 
 /**
  * A Resource is an immutable representation of the entity producing telemetry. For example, a process producing telemetry
@@ -18,89 +17,51 @@ use OpenTelemetry\SemConv\ResourceAttributes;
 class ResourceInfo
 {
     private AttributesInterface $attributes;
+    private ?string $schemaUrl;
 
-    private function __construct(AttributesInterface $attributes)
+    private function __construct(AttributesInterface $attributes, ?string $schemaUrl = null)
     {
         $this->attributes = $attributes;
+        $this->schemaUrl = $schemaUrl;
     }
 
-    public static function create(AttributesInterface $attributes): self
+    public static function create(AttributesInterface $attributes, ?string $schemaUrl = null): self
     {
-        /*
-         * The SDK MUST extract information from the OTEL_RESOURCE_ATTRIBUTES environment
-         * variable and merge this.
-         */
-        $resource = self::merge(
-            self::defaultResource(),
-            self::merge(
-                new ResourceInfo(clone $attributes),
-                self::environmentResource()
-            ),
-        );
-
-        return $resource;
+        return new ResourceInfo(clone $attributes, $schemaUrl);
     }
 
     /**
-     * Merges two resources into a new one.
-     * Conflicts (i.e. a key for which attributes exist on both the primary and secondary resource) are handled as follows:
-     * - If the value on the primary resource is an empty string, the result has the value of the secondary resource.
-     * - Otherwise, the value of the primary resource is used.
+     * Merges resources into a new one.
      *
-     * @param ResourceInfo $primary
-     * @param ResourceInfo $secondary
+     * @param ResourceInfo ...$resources
      * @return ResourceInfo
      */
-    public static function merge(ResourceInfo $primary, ResourceInfo $secondary): self
+    public static function merge(ResourceInfo ...$resources): self
     {
-        // clone attributes from the primary resource
-        $mergedAttributes = clone $primary->getAttributes();
+        $attributes = [];
+        $schemaUrl = null;
 
-        // merge attributes from the secondary resource
-        foreach ($secondary->getAttributes() as $name => $attribute) {
-            $mergedAttribute = $mergedAttributes->get($name);
-            if (null === $mergedAttribute || $mergedAttribute === '') {
-                $mergedAttributes->setAttribute($name, $attribute);
-            }
+        foreach ($resources as $resource) {
+            $attributes += $resource->getAttributes()->toArray();
+            $schemaUrl ??= $resource->getSchemaUrl();
         }
 
-        return new ResourceInfo($mergedAttributes);
+        return new ResourceInfo(new Attributes($attributes), $schemaUrl);
     }
 
     public static function defaultResource(): self
     {
-        return new ResourceInfo(new Attributes(
-            [
-                ResourceAttributes::TELEMETRY_SDK_NAME => 'opentelemetry',
-                ResourceAttributes::TELEMETRY_SDK_LANGUAGE => 'php',
-                ResourceAttributes::TELEMETRY_SDK_VERSION => 'dev',
-            ]
-        ));
-    }
+        return (new Detectors\Composite([
+            new Detectors\Environment(),
 
-    /**
-     * Create resource attributes from OTEL_RESOURCE_ATTRIBUTES key=value comma-separated entries
-     * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable
-     */
-    public static function environmentResource(): self
-    {
-        $attributes = [
-            ResourceAttributes::SERVICE_NAME => 'unknown_service',
-        ];
-        $string = getenv('OTEL_RESOURCE_ATTRIBUTES');
-        if ($string && false !== strpos($string, '=')) {
-            foreach (explode(',', $string) as $pair) {
-                [$key, $value] = explode('=', $pair);
-                $attributes[trim($key)] = trim($value);
-            }
-        }
-        //@see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#general-sdk-configuration
-        $serviceName = getenv('OTEL_SERVICE_NAME');
-        if ($serviceName) {
-            $attributes[ResourceAttributes::SERVICE_NAME] = $serviceName;
-        }
+            new Detectors\Host(),
+            new Detectors\OperatingSystem(),
+            new Detectors\Process(),
+            new Detectors\ProcessRuntime(),
 
-        return new ResourceInfo(new Attributes($attributes));
+            new Detectors\Sdk(),
+            new Detectors\SdkProvided(),
+        ]))->getResource();
     }
 
     public static function emptyResource(): self
@@ -112,4 +73,9 @@ class ResourceInfo
     {
         return $this->attributes;
     }
+
+    public function getSchemaUrl(): ?string
+    {
+        return $this->schemaUrl;
+    }
 }
diff --git a/src/SDK/Trace/TracerProvider.php b/src/SDK/Trace/TracerProvider.php
index 600fd462..3c1cf275 100644
--- a/src/SDK/Trace/TracerProvider.php
+++ b/src/SDK/Trace/TracerProvider.php
@@ -7,7 +7,6 @@ namespace OpenTelemetry\SDK\Trace;
 use function is_array;
 use OpenTelemetry\API\Trace as API;
 use OpenTelemetry\API\Trace\NoopTracer;
-use OpenTelemetry\SDK\Attributes;
 use OpenTelemetry\SDK\InstrumentationLibrary;
 use OpenTelemetry\SDK\Resource\ResourceInfo;
 use OpenTelemetry\SDK\Trace\Sampler\AlwaysOnSampler;
@@ -39,7 +38,7 @@ final class TracerProvider implements API\TracerProviderInterface
         }
 
         $spanProcessors = is_array($spanProcessors) ? $spanProcessors : [$spanProcessors];
-        $resource = $resource ?? ResourceInfo::create(new Attributes());
+        $resource = $resource ?? ResourceInfo::defaultResource();
         $sampler = $sampler ?? new ParentBased(new AlwaysOnSampler());
         $idGenerator = $idGenerator ?? new RandomIdGenerator();
         $spanLimits = $spanLimits ?? (new SpanLimitsBuilder())->build();


Test patch:
diff --git a/tests/Unit/Contrib/OTLPSpanConverterTest.php b/tests/Unit/Contrib/OTLPSpanConverterTest.php
index 6031ca00..fdc68793 100644
--- a/tests/Unit/Contrib/OTLPSpanConverterTest.php
+++ b/tests/Unit/Contrib/OTLPSpanConverterTest.php
@@ -153,7 +153,11 @@ class OTLPSpanConverterTest extends TestCase
             ->setResource(
                 ResourceInfo::create(
                     new Attributes([
+                        'telemetry.sdk.name' => 'opentelemetry',
+                        'telemetry.sdk.language' => 'php',
+                        'telemetry.sdk.version' => 'dev',
                         'instance' => 'test-a',
+                        'service.name' => 'unknown_service',
                     ])
                 )
             )
diff --git a/tests/Unit/SDK/Resource/ResourceTest.php b/tests/Unit/SDK/Resource/ResourceTest.php
index fc5a56fa..0fea433c 100644
--- a/tests/Unit/SDK/Resource/ResourceTest.php
+++ b/tests/Unit/SDK/Resource/ResourceTest.php
@@ -5,7 +5,9 @@ declare(strict_types=1);
 namespace OpenTelemetry\Tests\Unit\SDK\Resource;
 
 use AssertWell\PHPUnitGlobalState\EnvironmentVariables;
+use Composer\InstalledVersions;
 use OpenTelemetry\SDK\Attributes;
+use OpenTelemetry\SDK\Resource\Detectors;
 use OpenTelemetry\SDK\Resource\ResourceInfo;
 use OpenTelemetry\SemConv\ResourceAttributes;
 use PHPUnit\Framework\TestCase;
@@ -32,7 +34,14 @@ class ResourceTest extends TestCase
     {
         $attributes = new Attributes();
         $attributes->setAttribute('name', 'test');
-        $resource = ResourceInfo::create($attributes);
+
+        $resource = (new Detectors\Composite([
+            new Detectors\Constant(ResourceInfo::create($attributes)),
+            new Detectors\Sdk(),
+            new Detectors\SdkProvided(),
+        ]))->getResource();
+
+        $version = InstalledVersions::getVersion('open-telemetry/opentelemetry');
 
         $name = $resource->getAttributes()->get('name');
         $sdkname = $resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_NAME);
@@ -41,35 +50,42 @@ class ResourceTest extends TestCase
 
         $attributes->setAttribute(ResourceAttributes::TELEMETRY_SDK_NAME, 'opentelemetry');
         $attributes->setAttribute(ResourceAttributes::TELEMETRY_SDK_LANGUAGE, 'php');
-        $attributes->setAttribute(ResourceAttributes::TELEMETRY_SDK_VERSION, 'dev');
+        $attributes->setAttribute(ResourceAttributes::TELEMETRY_SDK_VERSION, $version);
         $attributes->setAttribute(ResourceAttributes::SERVICE_NAME, 'unknown_service');
 
         $this->assertEquals($attributes, $resource->getAttributes());
         $this->assertSame('opentelemetry', $sdkname);
         $this->assertSame('php', $sdklanguage);
-        $this->assertSame('dev', $sdkversion);
+        $this->assertSame($version, $sdkversion);
         $this->assertSame('test', $name);
     }
 
     public function test_default_resource(): void
     {
-        $attributes = new Attributes(
-            [
-                ResourceAttributes::TELEMETRY_SDK_NAME => 'opentelemetry',
-                ResourceAttributes::TELEMETRY_SDK_LANGUAGE => 'php',
-                ResourceAttributes::TELEMETRY_SDK_VERSION => 'dev',
-                ResourceAttributes::SERVICE_NAME => 'unknown_service',
-            ]
-        );
-        $resource = ResourceInfo::create(new Attributes());
-        $sdkname = $resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_NAME);
-        $sdklanguage = $resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_LANGUAGE);
-        $sdkversion = $resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_VERSION);
-
-        $this->assertEquals($attributes, $resource->getAttributes());
-        $this->assertEquals('opentelemetry', $sdkname);
-        $this->assertEquals('php', $sdklanguage);
-        $this->assertEquals('dev', $sdkversion);
+        $resource = ResourceInfo::defaultResource();
+
+        $this->assertSame(ResourceAttributes::SCHEMA_URL, $resource->getSchemaUrl());
+
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::HOST_NAME));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::HOST_ARCH));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::OS_TYPE));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::OS_DESCRIPTION));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::OS_NAME));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::OS_VERSION));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::PROCESS_PID));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::PROCESS_EXECUTABLE_PATH));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::PROCESS_COMMAND));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::PROCESS_COMMAND_ARGS));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::PROCESS_RUNTIME_NAME));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::PROCESS_RUNTIME_VERSION));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_NAME));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_LANGUAGE));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_VERSION));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME));
+
+        $this->assertEquals('opentelemetry', $resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_NAME));
+        $this->assertEquals('php', $resource->getAttributes()->get(ResourceAttributes::TELEMETRY_SDK_LANGUAGE));
+        $this->assertEquals('unknown_service', $resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME));
     }
 
     public function test_merge(): void
@@ -82,10 +98,10 @@ class ResourceTest extends TestCase
         $version = $result->getAttributes()->get('version');
         $empty = $result->getAttributes()->get('empty');
 
-        $this->assertCount(7, $result->getAttributes());
+        $this->assertCount(3, $result->getAttributes());
         $this->assertEquals('primary', $name);
         $this->assertEquals('1.0.0', $version);
-        $this->assertEquals('value', $empty);
+        $this->assertEquals('', $empty);
     }
 
     public function test_immutable_create(): void
@@ -109,7 +125,10 @@ class ResourceTest extends TestCase
     public function test_resource_from_environment(string $envAttributes, array $userAttributes, array $expected): void
     {
         $this->setEnvironmentVariable('OTEL_RESOURCE_ATTRIBUTES', $envAttributes);
-        $resource = ResourceInfo::create(new Attributes($userAttributes));
+        $resource = (new Detectors\Composite([
+            new Detectors\Constant(ResourceInfo::create(new Attributes($userAttributes))),
+            new Detectors\Environment(),
+        ]))->getResource();
         foreach ($expected as $name => $value) {
             $this->assertSame($value, $resource->getAttributes()->get($name));
         }
@@ -133,34 +152,42 @@ class ResourceTest extends TestCase
 
     public function test_resource_service_name_default(): void
     {
-        $resource = ResourceInfo::create(new Attributes([]));
+        $resource = ResourceInfo::defaultResource();
         $this->assertEquals('unknown_service', $resource->getAttributes()->get('service.name'));
     }
 
     public function test_resource_with_empty_environment_variable(): void
     {
         $this->setEnvironmentVariable('OTEL_RESOURCE_ATTRIBUTES', '');
-        $this->assertInstanceOf(ResourceInfo::class, ResourceInfo::create(new Attributes([])));
+        $this->assertInstanceOf(ResourceInfo::class, ResourceInfo::defaultResource());
     }
 
     public function test_resource_with_invalid_environment_variable(): void
     {
         $this->setEnvironmentVariable('OTEL_RESOURCE_ATTRIBUTES', 'foo');
-        $this->assertInstanceOf(ResourceInfo::class, ResourceInfo::create(new Attributes([])));
+        $this->assertInstanceOf(ResourceInfo::class, ResourceInfo::defaultResource());
     }
 
     public function test_resource_from_environment_service_name_takes_precedence_over_resource_attribute(): void
     {
         $this->setEnvironmentVariable('OTEL_RESOURCE_ATTRIBUTES', 'service.name=bar');
         $this->setEnvironmentVariable('OTEL_SERVICE_NAME', 'foo');
-        $resource = ResourceInfo::create(new Attributes([]));
+        $resource = ResourceInfo::defaultResource();
         $this->assertEquals('foo', $resource->getAttributes()->get('service.name'));
     }
 
     public function test_resource_from_environment_resource_attribute_takes_precedence_over_default(): void
     {
         $this->setEnvironmentVariable('OTEL_RESOURCE_ATTRIBUTES', 'service.name=foo');
-        $resource = ResourceInfo::create(new Attributes([]));
+        $resource = ResourceInfo::defaultResource();
         $this->assertEquals('foo', $resource->getAttributes()->get('service.name'));
     }
+
+    public function test_composer_detector(): void
+    {
+        $resource = (new Detectors\Composer())->getResource();
+
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_NAME));
+        $this->assertNotNull($resource->getAttributes()->get(ResourceAttributes::SERVICE_VERSION));
+    }
 }