Skip to content

Commit 2d18aac

Browse files
committed
feature: add git detector to telemetry
1 parent d00d301 commit 2d18aac

6 files changed

Lines changed: 320 additions & 1 deletion

File tree

src/lib/telemetry/src/Flow/Telemetry/DSL/functions.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
use Flow\Telemetry\Resource\Detector\ChainDetector;
7070
use Flow\Telemetry\Resource\Detector\ComposerDetector;
7171
use Flow\Telemetry\Resource\Detector\EnvironmentDetector;
72+
use Flow\Telemetry\Resource\Detector\GitDetector;
7273
use Flow\Telemetry\Resource\Detector\HostDetector;
7374
use Flow\Telemetry\Resource\Detector\ManualDetector;
7475
use Flow\Telemetry\Resource\Detector\OsDetector;
@@ -818,6 +819,17 @@ function composer_detector(): ComposerDetector
818819
return new ComposerDetector();
819820
}
820821

822+
/**
823+
* Create a GitDetector.
824+
*
825+
* @param null|string $workingDirectory Directory to run git in (default: current working directory)
826+
*/
827+
#[DocumentationDSL(module: Module::TELEMETRY, type: DSLType::HELPER)]
828+
function git_detector(?string $workingDirectory = null): GitDetector
829+
{
830+
return new GitDetector($workingDirectory);
831+
}
832+
821833
/**
822834
* Create a ManualDetector.
823835
*
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Telemetry\Resource\Attribute;
6+
7+
/**
8+
* VCS (Version Control System) attribute keys following OpenTelemetry semantic conventions.
9+
*
10+
* These attribute names correspond to the VCS resource attributes as defined
11+
* in the OpenTelemetry semantic conventions specification.
12+
*
13+
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/vcs/
14+
*/
15+
enum VcsAttribute: string
16+
{
17+
/**
18+
* The name of the reference that the HEAD points to.
19+
*
20+
* For a branch this is the branch name, for a tag the tag name.
21+
*
22+
* Example: "main", "v1.0.0"
23+
*/
24+
case REF_HEAD_NAME = 'vcs.ref.head.name';
25+
26+
/**
27+
* The revision (commit SHA) that the HEAD currently points to.
28+
*
29+
* Example: "9d59409acf479dfa0df1c3dad539b4825a8d2bcf"
30+
*/
31+
case REF_HEAD_REVISION = 'vcs.ref.head.revision';
32+
33+
/**
34+
* The type of the reference that the HEAD points to.
35+
*
36+
* Example: "branch", "tag"
37+
*/
38+
case REF_HEAD_TYPE = 'vcs.ref.head.type';
39+
40+
/**
41+
* The full URL of the repository providing the remote.
42+
*
43+
* Example: "https://github.com/flow-php/flow.git"
44+
*/
45+
case REPOSITORY_URL = 'vcs.repository.url.full';
46+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Telemetry\Resource\Detector;
6+
7+
use Flow\Telemetry\Resource;
8+
use Flow\Telemetry\Resource\Attribute\VcsAttribute;
9+
use Flow\Telemetry\Resource\ResourceDetector;
10+
11+
use function escapeshellarg;
12+
use function is_string;
13+
use function shell_exec;
14+
use function trim;
15+
16+
/**
17+
* Detects Git (VCS) information by shelling out to the `git` binary.
18+
*
19+
* Detects the following attributes (OpenTelemetry vcs.* semantic conventions):
20+
* - vcs.ref.head.revision: The current commit SHA
21+
* - vcs.ref.head.name: The current branch or tag name (omitted when HEAD is detached on an untagged commit)
22+
* - vcs.ref.head.type: The reference type ("branch" or "tag")
23+
* - vcs.repository.url.full: The remote "origin" URL
24+
*
25+
* Requires the `git` binary to be available and the working directory (or the
26+
* provided one) to be inside a Git work tree. When neither is the case, an empty
27+
* Resource is returned.
28+
*
29+
* Example output:
30+
* ```
31+
* vcs.ref.head.revision: 9d59409acf479dfa0df1c3dad539b4825a8d2bcf
32+
* vcs.ref.head.name: main
33+
* vcs.ref.head.type: branch
34+
* vcs.repository.url.full: https://github.com/flow-php/flow.git
35+
* ```
36+
*/
37+
final readonly class GitDetector implements ResourceDetector
38+
{
39+
public function __construct(
40+
private ?string $workingDirectory = null,
41+
) {}
42+
43+
public function detect(): Resource
44+
{
45+
if ($this->runGit('rev-parse --is-inside-work-tree') !== 'true') {
46+
return Resource::empty();
47+
}
48+
49+
$attributes = [];
50+
51+
$revision = $this->runGit('rev-parse HEAD');
52+
53+
if ($revision !== null) {
54+
$attributes[VcsAttribute::REF_HEAD_REVISION->value] = $revision;
55+
}
56+
57+
$branch = $this->runGit('rev-parse --abbrev-ref HEAD');
58+
59+
if ($branch !== null && $branch !== 'HEAD') {
60+
$attributes[VcsAttribute::REF_HEAD_NAME->value] = $branch;
61+
$attributes[VcsAttribute::REF_HEAD_TYPE->value] = 'branch';
62+
} else {
63+
// Detached HEAD: resolve a tag pointing at the current commit, if any.
64+
$tag = $this->runGit('describe --tags --exact-match');
65+
66+
if ($tag !== null) {
67+
$attributes[VcsAttribute::REF_HEAD_NAME->value] = $tag;
68+
$attributes[VcsAttribute::REF_HEAD_TYPE->value] = 'tag';
69+
}
70+
}
71+
72+
$repositoryUrl = $this->runGit('git remote get-url origin');
73+
74+
if ($repositoryUrl !== null) {
75+
$attributes[VcsAttribute::REPOSITORY_URL->value] = $repositoryUrl;
76+
}
77+
78+
return Resource::create($attributes);
79+
}
80+
81+
private function runGit(string $arguments): ?string
82+
{
83+
$command = 'git ';
84+
85+
if ($this->workingDirectory !== null) {
86+
$command .= '-C ' . escapeshellarg($this->workingDirectory) . ' ';
87+
}
88+
89+
$command .= $arguments . ' 2>/dev/null';
90+
91+
$output = @shell_exec($command);
92+
93+
if (!is_string($output)) {
94+
return null;
95+
}
96+
97+
$output = trim($output);
98+
99+
return $output === '' ? null : $output;
100+
}
101+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Telemetry\Tests\Integration\Resource\Detector;
6+
7+
use Flow\Telemetry\Resource\Attribute\VcsAttribute;
8+
use Flow\Telemetry\Resource\Detector\GitDetector;
9+
use PHPUnit\Framework\TestCase;
10+
11+
use function is_string;
12+
use function shell_exec;
13+
use function sys_get_temp_dir;
14+
use function uniqid;
15+
16+
final class GitDetectorTest extends TestCase
17+
{
18+
private GitRepositoryFactory $gitRepositoryFactory;
19+
20+
protected function setUp(): void
21+
{
22+
$this->skipIfGitBinaryNotExists();
23+
24+
$this->gitRepositoryFactory = new GitRepositoryFactory();
25+
}
26+
27+
protected function tearDown(): void
28+
{
29+
$this->gitRepositoryFactory->cleanup();
30+
}
31+
32+
public function test_detect_returns_empty_resource_outside_a_work_tree(): void
33+
{
34+
$directory = sys_get_temp_dir() . '/flow_telemetry_git_test_' . uniqid();
35+
36+
$resource = (new GitDetector($directory))->detect();
37+
38+
static::assertSame(0, $resource->count());
39+
}
40+
41+
public function test_detect_returns_head_revision(): void
42+
{
43+
$directory = $this->gitRepositoryFactory->createWithBranch('my-branch');
44+
45+
$resource = (new GitDetector($directory))->detect();
46+
47+
$revision = $resource->get(VcsAttribute::REF_HEAD_REVISION->value);
48+
49+
static::assertIsString($revision);
50+
static::assertMatchesRegularExpression('/^[0-9a-f]{40}$/', $revision);
51+
}
52+
53+
public function test_detect_returns_branch_name_and_type_when_not_detached(): void
54+
{
55+
$directory = $this->gitRepositoryFactory->createWithBranch('my-branch');
56+
57+
$resource = (new GitDetector($directory))->detect();
58+
59+
static::assertSame('my-branch', $resource->get(VcsAttribute::REF_HEAD_NAME->value));
60+
static::assertSame('branch', $resource->get(VcsAttribute::REF_HEAD_TYPE->value));
61+
}
62+
63+
public function test_detect_returns_tag_name_and_type_in_detached_head(): void
64+
{
65+
$directory = $this->gitRepositoryFactory->createWithTag('v1.2.3');
66+
67+
$resource = (new GitDetector($directory))->detect();
68+
69+
static::assertSame('v1.2.3', $resource->get(VcsAttribute::REF_HEAD_NAME->value));
70+
static::assertSame('tag', $resource->get(VcsAttribute::REF_HEAD_TYPE->value));
71+
}
72+
73+
private function skipIfGitBinaryNotExists(): void
74+
{
75+
if (!is_string(@shell_exec('git --version 2>/dev/null'))) {
76+
static::markTestSkipped('Git binary is unavailable');
77+
}
78+
}
79+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Telemetry\Tests\Integration\Resource\Detector;
6+
7+
use LogicException;
8+
9+
use function escapeshellarg;
10+
use function shell_exec;
11+
use function sys_get_temp_dir;
12+
use function uniqid;
13+
14+
final class GitRepositoryFactory
15+
{
16+
/**
17+
* @var array<string>
18+
*/
19+
private array $temporaryDirectories = [];
20+
21+
public function createWithBranch(string $branch): string
22+
{
23+
if (trim($branch) === '') {
24+
throw new LogicException('Branch should not be empty');
25+
}
26+
27+
return $this->initialize(branch: $branch);
28+
}
29+
30+
public function createWithTag(string $tag): string
31+
{
32+
if (trim($tag) === '') {
33+
throw new LogicException('Tag should not be empty');
34+
}
35+
36+
return $this->initialize(tag: $tag);
37+
}
38+
39+
public function cleanup(): void
40+
{
41+
foreach ($this->temporaryDirectories as $directory) {
42+
@shell_exec('rm -rf ' . escapeshellarg($directory));
43+
}
44+
45+
$this->temporaryDirectories = [];
46+
}
47+
48+
private function initialize(?string $branch = null, ?string $tag = null): string
49+
{
50+
if ($branch === null && $tag === null) {
51+
throw new LogicException('Either a branch or a tag must be provided');
52+
}
53+
54+
if ($branch !== null && $tag !== null) {
55+
throw new LogicException('Provide either a branch or a tag, not both');
56+
}
57+
58+
$directory = sys_get_temp_dir() . '/flow_telemetry_git_tag_' . uniqid();
59+
$this->temporaryDirectories[] = $directory;
60+
61+
$git = 'git -C ' . escapeshellarg($directory);
62+
63+
@shell_exec('mkdir -p ' . escapeshellarg($directory));
64+
@shell_exec($git . ' init -q 2>/dev/null');
65+
@shell_exec($git . ' config user.email "test@example.com" 2>/dev/null');
66+
@shell_exec($git . ' config user.name "Test" 2>/dev/null');
67+
@shell_exec($git . ' commit --allow-empty -m "init" -q 2>/dev/null');
68+
69+
if ($branch !== null) {
70+
@shell_exec($git . ' branch -c ' . escapeshellarg($branch) . ' 2>/dev/null');
71+
@shell_exec($git . ' checkout -q ' . escapeshellarg($branch) . ' 2>/dev/null');
72+
}
73+
74+
if ($tag !== null) {
75+
@shell_exec($git . ' tag ' . escapeshellarg($tag) . ' 2>/dev/null');
76+
@shell_exec($git . ' checkout -q ' . escapeshellarg($tag) . ' 2>/dev/null');
77+
}
78+
79+
return $directory;
80+
}
81+
}

web/landing/resources/dsl.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)