Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use function Flow\Telemetry\DSL\batching_log_processor;
use function Flow\Telemetry\DSL\batching_metric_processor;
use function Flow\Telemetry\DSL\batching_span_processor;
use function Flow\Telemetry\DSL\git_detector;
use function Flow\Telemetry\DSL\logger_provider;
use function Flow\Telemetry\DSL\memory_context_storage;
use function Flow\Telemetry\DSL\meter_provider;
Expand All @@ -39,6 +40,7 @@ public static function create(Configuration $config): Telemetry
{
$telemetryResource = resource_detector()
->detect()
->merge(git_detector()->detect())
->merge(resource([
'service.name' => $config->serviceName,
'telemetry.sdk.name' => 'flow-php-phpunit-telemetry',
Expand Down
12 changes: 12 additions & 0 deletions src/lib/telemetry/src/Flow/Telemetry/DSL/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
use Flow\Telemetry\Resource\Detector\ChainDetector;
use Flow\Telemetry\Resource\Detector\ComposerDetector;
use Flow\Telemetry\Resource\Detector\EnvironmentDetector;
use Flow\Telemetry\Resource\Detector\GitDetector;
use Flow\Telemetry\Resource\Detector\HostDetector;
use Flow\Telemetry\Resource\Detector\ManualDetector;
use Flow\Telemetry\Resource\Detector\OsDetector;
Expand Down Expand Up @@ -818,6 +819,17 @@ function composer_detector(): ComposerDetector
return new ComposerDetector();
}

/**
* Create a GitDetector.
*
* @param null|string $workingDirectory Directory to run git in (default: current working directory)
*/
#[DocumentationDSL(module: Module::TELEMETRY, type: DSLType::HELPER)]
function git_detector(?string $workingDirectory = null): GitDetector
{
return new GitDetector($workingDirectory);
}

/**
* Create a ManualDetector.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Flow\Telemetry\Resource\Attribute;

/**
* VCS (Version Control System) attribute keys following OpenTelemetry semantic conventions.
*
* These attribute names correspond to the VCS resource attributes as defined
* in the OpenTelemetry semantic conventions specification.
*
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/vcs/
*/
enum VcsAttribute: string
{
/**
* The name of the reference that the HEAD points to.
*
* For a branch this is the branch name, for a tag the tag name.
*
* Example: "main", "v1.0.0"
*/
case REF_HEAD_NAME = 'vcs.ref.head.name';

/**
* The revision (commit SHA) that the HEAD currently points to.
*
* Example: "9d59409acf479dfa0df1c3dad539b4825a8d2bcf"
*/
case REF_HEAD_REVISION = 'vcs.ref.head.revision';

/**
* The type of the reference that the HEAD points to.
*
* Example: "branch", "tag"
*/
case REF_HEAD_TYPE = 'vcs.ref.head.type';

/**
* The full URL of the repository providing the remote.
*
* Example: "https://github.com/flow-php/flow.git"
*/
case REPOSITORY_URL = 'vcs.repository.url.full';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Flow\Telemetry\Resource\Detector;

use Flow\Telemetry\Resource;
use Flow\Telemetry\Resource\Attribute\VcsAttribute;
use Flow\Telemetry\Resource\ResourceDetector;

use function escapeshellarg;
use function is_string;
use function shell_exec;
use function trim;

/**
* Detects Git (VCS) information by shelling out to the `git` binary.
*
* Detects the following attributes (OpenTelemetry vcs.* semantic conventions):
* - vcs.ref.head.revision: The current commit SHA
* - vcs.ref.head.name: The current branch or tag name (omitted when HEAD is detached on an untagged commit)
* - vcs.ref.head.type: The reference type ("branch" or "tag")
* - vcs.repository.url.full: The remote "origin" URL
*
* Requires the `git` binary to be available and the working directory (or the
* provided one) to be inside a Git work tree. When neither is the case, an empty
* Resource is returned.
*
* Example output:
* ```
* vcs.ref.head.revision: 9d59409acf479dfa0df1c3dad539b4825a8d2bcf
* vcs.ref.head.name: main
* vcs.ref.head.type: branch
* vcs.repository.url.full: https://github.com/flow-php/flow.git
* ```
*/
final readonly class GitDetector implements ResourceDetector
{
public function __construct(
private ?string $workingDirectory = null,
) {}

public function detect(): Resource
{
if ($this->runGit('rev-parse --is-inside-work-tree') !== 'true') {
return Resource::empty();
}

$attributes = [];

$revision = $this->runGit('rev-parse HEAD');

if ($revision !== null) {
$attributes[VcsAttribute::REF_HEAD_REVISION->value] = $revision;
}

$branch = $this->runGit('rev-parse --abbrev-ref HEAD');

if ($branch !== null && $branch !== 'HEAD') {
$attributes[VcsAttribute::REF_HEAD_NAME->value] = $branch;
$attributes[VcsAttribute::REF_HEAD_TYPE->value] = 'branch';
} else {
// Detached HEAD: resolve a tag pointing at the current commit, if any.
$tag = $this->runGit('describe --tags --exact-match');

if ($tag !== null) {
$attributes[VcsAttribute::REF_HEAD_NAME->value] = $tag;
$attributes[VcsAttribute::REF_HEAD_TYPE->value] = 'tag';
}
}

$repositoryUrl = $this->runGit('git remote get-url origin');

if ($repositoryUrl !== null) {
$attributes[VcsAttribute::REPOSITORY_URL->value] = $repositoryUrl;
}

return Resource::create($attributes);
}

private function runGit(string $arguments): ?string
{
$command = 'git ';

if ($this->workingDirectory !== null) {
$command .= '-C ' . escapeshellarg($this->workingDirectory) . ' ';
}

$command .= $arguments . ' 2>/dev/null';

$output = @shell_exec($command);

if (!is_string($output)) {
return null;
}

$output = trim($output);

return $output === '' ? null : $output;
Comment on lines +81 to +99
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let me think about this one, it might expose some security details.
For example here: $repositoryUrl = $this->runGit('git remote get-url origin');
If one is keeping it like this https://user:password@host/repo.git.

It also seems that $this->runGit('git remote get-url origin'); would create git git remote get-url origin command.

Another issue I see here is that git is resolved from $PATH which might not be the always the case so users should be able to pass also additional git binary path.

I'm also not sure that shell_exec is the best option here, I would probably try to do it with proc_open as it does not need to even go through shell

private function runGit(array $args): ?string
{
    $cmd = ['git'];
    if ($this->workingDirectory !== null) {
        $cmd[] = '-C';
        $cmd[] = $this->workingDirectory;
    }
    $cmd = [...$cmd, ...$args];
    // run with proc_open / Process, no shell
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Flow\Telemetry\Tests\Integration\Resource\Detector;

use Flow\Telemetry\Resource\Attribute\VcsAttribute;
use Flow\Telemetry\Resource\Detector\GitDetector;
use PHPUnit\Framework\TestCase;

use function is_string;
use function shell_exec;
use function sys_get_temp_dir;
use function uniqid;

final class GitDetectorTest extends TestCase
{
private GitRepositoryFactory $gitRepositoryFactory;

protected function setUp(): void
{
$this->skipIfGitBinaryNotExists();

$this->gitRepositoryFactory = new GitRepositoryFactory();
}

protected function tearDown(): void
{
$this->gitRepositoryFactory->cleanup();
}

public function test_detect_returns_empty_resource_outside_a_work_tree(): void
{
$directory = sys_get_temp_dir() . '/flow_telemetry_git_test_' . uniqid();

$resource = (new GitDetector($directory))->detect();

static::assertSame(0, $resource->count());
}

public function test_detect_returns_head_revision(): void
{
$directory = $this->gitRepositoryFactory->createWithBranch('my-branch');

$resource = (new GitDetector($directory))->detect();

$revision = $resource->get(VcsAttribute::REF_HEAD_REVISION->value);

static::assertIsString($revision);
static::assertMatchesRegularExpression('/^[0-9a-f]{40}$/', $revision);
}

public function test_detect_returns_branch_name_and_type_when_not_detached(): void
{
$directory = $this->gitRepositoryFactory->createWithBranch('my-branch');

$resource = (new GitDetector($directory))->detect();

static::assertSame('my-branch', $resource->get(VcsAttribute::REF_HEAD_NAME->value));
static::assertSame('branch', $resource->get(VcsAttribute::REF_HEAD_TYPE->value));
}

public function test_detect_returns_tag_name_and_type_in_detached_head(): void
{
$directory = $this->gitRepositoryFactory->createWithTag('v1.2.3');

$resource = (new GitDetector($directory))->detect();

static::assertSame('v1.2.3', $resource->get(VcsAttribute::REF_HEAD_NAME->value));
static::assertSame('tag', $resource->get(VcsAttribute::REF_HEAD_TYPE->value));
}

private function skipIfGitBinaryNotExists(): void
{
if (!is_string(@shell_exec('git --version 2>/dev/null'))) {
static::markTestSkipped('Git binary is unavailable');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Flow\Telemetry\Tests\Integration\Resource\Detector;

use LogicException;

use function escapeshellarg;
use function shell_exec;
use function sys_get_temp_dir;
use function uniqid;

final class GitRepositoryFactory
{
/**
* @var array<string>
*/
private array $temporaryDirectories = [];

public function createWithBranch(string $branch): string
{
if (trim($branch) === '') {
throw new LogicException('Branch should not be empty');
}

return $this->initialize(branch: $branch);
}

public function createWithTag(string $tag): string
{
if (trim($tag) === '') {
throw new LogicException('Tag should not be empty');
}

return $this->initialize(tag: $tag);
}

public function cleanup(): void
{
foreach ($this->temporaryDirectories as $directory) {
@shell_exec('rm -rf ' . escapeshellarg($directory));
}

$this->temporaryDirectories = [];
}

private function initialize(?string $branch = null, ?string $tag = null): string
{
if ($branch === null && $tag === null) {
throw new LogicException('Either a branch or a tag must be provided');
}

if ($branch !== null && $tag !== null) {
throw new LogicException('Provide either a branch or a tag, not both');
}

$directory = sys_get_temp_dir() . '/flow_telemetry_git_tag_' . uniqid();
$this->temporaryDirectories[] = $directory;

$git = 'git -C ' . escapeshellarg($directory);

@shell_exec('mkdir -p ' . escapeshellarg($directory));
@shell_exec($git . ' init -q 2>/dev/null');
@shell_exec($git . ' config user.email "test@example.com" 2>/dev/null');
@shell_exec($git . ' config user.name "Test" 2>/dev/null');
@shell_exec($git . ' commit --allow-empty -m "init" -q 2>/dev/null');

if ($branch !== null) {
@shell_exec($git . ' branch -c ' . escapeshellarg($branch) . ' 2>/dev/null');
@shell_exec($git . ' checkout -q ' . escapeshellarg($branch) . ' 2>/dev/null');
}

if ($tag !== null) {
@shell_exec($git . ' tag ' . escapeshellarg($tag) . ' 2>/dev/null');
@shell_exec($git . ' checkout -q ' . escapeshellarg($tag) . ' 2>/dev/null');
}
Comment on lines +63 to +77
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm honestly not very comfortable with all those shell execs in the test suite.
I'm also tryign to use local __DIR__ . /var folders close to tests rather than sys_get_temp_dir.

Personally I think it would be better if those git repositories would live in some Fixtures folder in the integration test suite instead of creating them on demand


return $directory;
}
}
2 changes: 1 addition & 1 deletion web/landing/resources/dsl.json

Large diffs are not rendered by default.

Loading