diff --git a/composer.json b/composer.json
index 800495975d302ab95be370d11665abbb9ec75150..ed045f08b53d2ae82fb9a304c1451da18585c429 100644
--- a/composer.json
+++ b/composer.json
@@ -20,6 +20,9 @@
         "open-telemetry/api": "^1.1",
         "open-telemetry/sem-conv": "^1.0"
     },
+    "require-dev": {
+        "drupal/otunit": "@dev"
+    },
     "suggest": {
         "open-telemetry/sdk": "To perform zero-code OpenTelemetry instrumentation"
     }
diff --git a/tests/modules/otlrs_test/otlog_test.info.yml b/tests/modules/otlrs_test/otlog_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ceb8aa700c743fc337b54fc44fabd31db7b2b39b
--- /dev/null
+++ b/tests/modules/otlrs_test/otlog_test.info.yml
@@ -0,0 +1,4 @@
+name: 'otlrs test'
+type: module
+description: 'Support module for otlrs testing.'
+package: Testing
diff --git a/tests/modules/otlrs_test/otlog_test.routing.yml b/tests/modules/otlrs_test/otlog_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cd9d1e2fd6be44ff65ee8571447555e9b9c3e968
--- /dev/null
+++ b/tests/modules/otlrs_test/otlog_test.routing.yml
@@ -0,0 +1,13 @@
+otlog_test.debug:
+  path: '/otlog_test/debug'
+  defaults:
+    _controller: '\Drupal\otlog_test\TestControllers::debug'
+  requirements:
+    _access: 'TRUE'
+
+otlog_test.exception:
+  path: '/otlog_test/error'
+  defaults:
+    _controller: '\Drupal\otlog_test\TestControllers::exception'
+  requirements:
+    _access: 'TRUE'
diff --git a/tests/modules/otlrs_test/src/TestControllers.php b/tests/modules/otlrs_test/src/TestControllers.php
new file mode 100644
index 0000000000000000000000000000000000000000..2b58f29eaa951ae7629d1b13f1846e8a5228051a
--- /dev/null
+++ b/tests/modules/otlrs_test/src/TestControllers.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\otlog_test;
+
+use Drupal\Core\Controller\ControllerBase;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Controller routines for testing otlog module.
+ */
+class TestControllers extends ControllerBase {
+
+  /**
+   * Returns test response.
+   */
+  public function debug() {
+    $this->getLogger('test controller')->debug('test {level} message', [
+      'level' => 'debug',
+    ]);
+    return new Response('ok');
+  }
+
+  /**
+   * Throws test exception.
+   */
+  public function exception() {
+    throw new \RuntimeException('test exception');
+  }
+
+}
diff --git a/tests/src/Functional/LogIntegrationTest.php b/tests/src/Functional/LogIntegrationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6043c7907aebc8853f17c8247b2305505e517216
--- /dev/null
+++ b/tests/src/Functional/LogIntegrationTest.php
@@ -0,0 +1,115 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\otlog\Functional;
+
+use Drupal\Component\OTUnit\FileExporterTrait;
+use Drupal\Component\OTUnit\ProtoHelpersTrait;
+use Drupal\Component\OTUnit\SdkAutoloadingTrait;
+use Drupal\otlog\Logger\OtLog;
+use Drupal\Tests\BrowserTestBase;
+use Opentelemetry\Proto\Logs\V1\LogRecord;
+use OpenTelemetry\SemConv\TraceAttributes;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
+
+/**
+ * End-to-end test for OpenTelemetry SDK autoloading and configuration.
+ */
+#[Group('otlog')]
+#[CoversClass(OtLog::class)]
+final class LogIntegrationTest extends BrowserTestBase {
+
+  use FileExporterTrait;
+  use ProtoHelpersTrait;
+  use SdkAutoloadingTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['otlog', 'otlog_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->setupSdkAutoloading($this->getFileExporterConfigurationVariables());
+  }
+
+  /**
+   * Tests a controller generating a debug message.
+   */
+  public function testDebugMessage(): void {
+    $this->resetFileExporterResultFiles();
+    $this->drupalGet('/otlog_test/debug');
+
+    [$logMessage] = $this->waitForLogs();
+
+    // Check resource attribute.
+    $traceResource = $this->getLogResource($logMessage);
+    $sdkName = $this->getAttributeValue(
+      TraceAttributes::TELEMETRY_SDK_NAME,
+      $traceResource->getAttributes()
+    );
+    $this->assertNotNull($sdkName);
+    $this->assertTrue($sdkName->hasStringValue());
+    $this->assertEquals('opentelemetry', $sdkName->getStringValue());
+
+    // Check scope.
+    $scope = $this->getLogScope($logMessage);
+    $this->assertEquals(
+      'org.drupal.otlog',
+      $scope->getName()
+    );
+
+    $logs = $this->getLogRecords($logMessage);
+    assert($logs[0] instanceof LogRecord);
+
+    $body = $logs[0]->getBody();
+    $this->assertNotNull($body);
+    $this->assertTrue($body->hasStringValue());
+    $this->assertEquals('[test controller] [debug] test debug message', $body->getStringValue());
+
+    $this->assertEquals(5, $logs[0]->getSeverityNumber());
+    $this->assertEquals('debug', $logs[0]->getSeverityText());
+  }
+
+  /**
+   * Tests controller causing an uncaught exception.
+   */
+  public function testException(): void {
+    $this->resetFileExporterResultFiles();
+    $this->drupalGet('/otlog_test/error');
+
+    [$logMessage] = $this->waitForLogs();
+
+    $logs = $this->getLogRecords($logMessage);
+    assert($logs[0] instanceof LogRecord);
+
+    $body = $logs[0]->getBody();
+    $this->assertNotNull($body);
+    $this->assertTrue($body->hasStringValue());
+    $this->assertStringStartsWith('[php] [error] RuntimeException: test exception in Drupal\otlog_test\TestControllers->exception() (line ', $body->getStringValue());
+    $this->assertStringEndsWith('/otlrs_test/src/TestControllers.php).', $body->getStringValue());
+
+    $this->assertEquals(17, $logs[0]->getSeverityNumber());
+    $this->assertEquals('error', $logs[0]->getSeverityText());
+
+    $codeStacktrace = $this->getAttributeValue(
+      TraceAttributes::CODE_STACKTRACE,
+      $logs[0]->getAttributes(),
+    );
+    $this->assertNotNull($codeStacktrace);
+    $this->assertTrue($codeStacktrace->hasStringValue());
+    $this->assertStringContainsString('TestControllers->exception()', $codeStacktrace->getStringValue());
+  }
+
+}