-
-
Notifications
You must be signed in to change notification settings - Fork 52
Add git detector to telemetry #2409
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
base: 1.x
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
| } | ||
| 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
Member
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'm honestly not very comfortable with all those shell execs in the test suite. Personally I think it would be better if those git repositories would live in some |
||
|
|
||
| return $directory; | ||
| } | ||
| } | ||
Large diffs are not rendered by default.
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.
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 creategit git remote get-url origincommand.Another issue I see here is that
gitis resolved from$PATHwhich 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_execis the best option here, I would probably try to do it with proc_open as it does not need to even go through shell