Skip to content

Commit

Permalink
Merge branch 'webgrip:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryangr0 authored Oct 15, 2024
2 parents fc411f6 + d371959 commit d84a8a0
Show file tree
Hide file tree
Showing 14 changed files with 402 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
vendor/
var/
.composer/
.phpunit.cache/
.env
*.private.env.json
auth.json
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,38 @@ class Foo
}
}
```


### Using attributes
> You can add the attribute 'Webgrip\TelemetryService\Core\Domain\Services\Traceable' to your class to automatically trace all methods of the class
> You can also use this attribute to trace a single method
```php

#[\Webgrip\TelemetryService\Core\Domain\Attributes\Traceable]
class Foo
{
public function bar()
{
// ...
}

#[\Webgrip\TelemetryService\Core\Domain\Attributes\Traceable]
public function baz()
{
// ...
}
}
```
}

// DI configuration
return [
Foo::class => function (\DI\Container $container) {
$factory = $container->get(\Webgrip\TelemetryService\Infrastructure\Factories\TracedClassFactory::class)
$foo = new Foo();

return $factory->create($foo);
}
];
```
13 changes: 13 additions & 0 deletions src/Core/Domain/Attributes/Traceable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Webgrip\TelemetryService\Core\Domain\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class Traceable
{
public function __construct(
public ?string $operationName = null
) {}
}
5 changes: 5 additions & 0 deletions src/Core/Domain/Services/TelemetryServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public function tracer(): TracerInterface;
* @return void
*/
public function registerException(\Throwable $exception, SpanInterface $span): void;

/**
* @return SpanInterface
*/
public function getCurrentSpan(): SpanInterface;
}
11 changes: 11 additions & 0 deletions src/Core/Domain/Services/TracingProxyInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Webgrip\TelemetryService\Core\Domain\Services;

use OpenTelemetry\API\Trace\TracerInterface;

interface TracingProxyInterface
{
public function __construct(object $instance, TracerInterface $tracer);
public function __call(string $method, array $arguments): mixed;
}
1 change: 0 additions & 1 deletion src/Infrastructure/Factories/TelemetryServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Webgrip\TelemetryService\Core\Application\Factories\TelemetryServiceFactoryInterface;
use Webgrip\TelemetryService\Core\Application\Factories\TracerProviderFactoryInterface;
use Webgrip\TelemetryService\Infrastructure\Services\TelemetryService;
use Webgrip\TelemetryService\Infrastructure\Telemetry\NoopOpenTelemetryCollector;

final class TelemetryServiceFactory implements TelemetryServiceFactoryInterface
{
Expand Down
31 changes: 31 additions & 0 deletions src/Infrastructure/Factories/TracedClassFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Webgrip\TelemetryService\Infrastructure\Factories;

use ReflectionClass;
use Webgrip\TelemetryService\Core\Domain\Attributes\Traceable;
use Webgrip\TelemetryService\Core\Domain\Services\TelemetryServiceInterface;
use Webgrip\TelemetryService\Infrastructure\Services\TracingProxy;

class TracedClassFactory
{
private TelemetryServiceInterface $telemetryService;

public function __construct(TelemetryServiceInterface $telemetryService)
{
$this->telemetryService = $telemetryService;
}

public function create(object $instance): object
{
$reflectionClass = new ReflectionClass($instance);
$hasTraceableAttribute = !empty($reflectionClass->getAttributes(Traceable::class));

if ($hasTraceableAttribute) {
return new TracingProxy($instance, $this->telemetryService);
}

// If no Traceable attribute, return the instance directly
return $instance;
}
}
9 changes: 9 additions & 0 deletions src/Infrastructure/Services/TelemetryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Monolog\Level;
use Monolog\Logger;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\API\Trace\TracerInterface;
Expand Down Expand Up @@ -55,4 +56,12 @@ public function registerException(\Throwable $exception, SpanInterface $span): v
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
$span->recordException($exception);
}

/**
* @return SpanInterface
*/
public function getCurrentSpan(): SpanInterface
{
return Span::getCurrent();
}
}
77 changes: 77 additions & 0 deletions src/Infrastructure/Services/TracingProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Webgrip\TelemetryService\Infrastructure\Services;

use OpenTelemetry\API\Trace\SpanInterface;
use ReflectionClass;
use ReflectionMethod;
use Webgrip\TelemetryService\Core\Domain\Attributes\Traceable;
use Webgrip\TelemetryService\Core\Domain\Services\TelemetryServiceInterface;

class TracingProxy
{
public TelemetryServiceInterface $telemetryService;
private object $instance;

public SpanInterface $span;


private bool $traceAllMethods;

/**
* @param object $instance
* @param TelemetryServiceInterface $telemetryService
*/
public function __construct(object $instance, TelemetryServiceInterface $telemetryService)
{
$this->instance = $instance;
$this->telemetryService = $telemetryService;

// Check if the class itself is marked as Traceable
$reflectionClass = new ReflectionClass($this->instance);
$this->traceAllMethods = !empty($reflectionClass->getAttributes(Traceable::class));
}

/**
* @param string $method
* @param array $arguments
* @return mixed
* @throws \ReflectionException
* @throws \Throwable
*/
public function __call(string $method, array $arguments)
{
$reflectionMethod = new ReflectionMethod($this->instance, $method);
$traceableAttributes = $reflectionMethod->getAttributes(Traceable::class);
$traceableMethod = !empty($traceableAttributes);

// Set operation name from attribute or use the method name as fallback
$operationName = $method;
if ($traceableMethod) {
/** @var Traceable $traceableInstance */
$traceableInstance = $traceableAttributes[0]->newInstance();
$operationName = $traceableInstance->operationName ?? $method;
}

if ($this->traceAllMethods || $traceableMethod) {
$this->span = $this->telemetryService->tracer()
->spanBuilder($operationName)
->startSpan();

$scope = $this->span->activate();

try {
// Invoke the original method with the arguments
return $reflectionMethod->invokeArgs($this->instance, $arguments);
} catch (\Throwable $e) {
$this->telemetryService->registerException($e, $this->span);
throw $e;
} finally {
$scope->detach();
$this->span->end();
}
} else {
return $reflectionMethod->invokeArgs($this->instance, $arguments);
}
}
}
11 changes: 11 additions & 0 deletions tests/Fixtures/NonTraceableClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Webgrip\TelemetryService\Tests\Fixtures;

class NonTraceableClass
{
public function untracedMethod()
{
// Simulate some logic
}
}
19 changes: 19 additions & 0 deletions tests/Fixtures/PartiallyTraceableClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Webgrip\TelemetryService\Tests\Fixtures;

use Webgrip\TelemetryService\Core\Domain\Attributes\Traceable;

class PartiallyTraceableClass
{
#[Traceable]
public function tracedMethod()
{
// Simulate some logic
}

public function untracedMethod()
{
// Simulate some logic
}
}
14 changes: 14 additions & 0 deletions tests/Fixtures/TraceableClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Webgrip\TelemetryService\Tests\Fixtures;

use Webgrip\TelemetryService\Core\Domain\Attributes\Traceable;

#[Traceable]
class TraceableClass
{
public function tracedMethod()
{
return 'traced';
}
}
51 changes: 51 additions & 0 deletions tests/Unit/Core/Domain/Attributes/TraceableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Unit\Core\Domain\Attributes;

use ReflectionClass;
use ReflectionMethod;
use Webgrip\TelemetryService\Core\Domain\Attributes\Traceable;
use Webgrip\TelemetryService\Tests\Fixtures\NonTraceableClass;
use Webgrip\TelemetryService\Tests\Fixtures\PartiallyTraceableClass;
use Webgrip\TelemetryService\Tests\Fixtures\TraceableClass;
use Webgrip\TelemetryService\Tests\Unit\TestCase;

class TraceableTest extends TestCase
{
public function testClassHasTraceableAttribute()
{
$reflection = new ReflectionClass(TraceableClass::class);
$attributes = $reflection->getAttributes(Traceable::class);

$this->assertNotEmpty($attributes, "Class 'ExampleClass' should have Traceable attribute");
}

public function testMethodHasTraceableAttribute()
{
$partiallyTracableClass = new ReflectionClass(PartiallyTraceableClass::class);
$tracedReflection = new ReflectionMethod(PartiallyTraceableClass::class, 'tracedMethod');
$untracedReflection = new ReflectionMethod(PartiallyTraceableClass::class, 'untracedMethod');;

$this->assertEmpty(
$partiallyTracableClass->getAttributes(Traceable::class),
"Method 'tracedMethod' should have Traceable attribute"
);

$this->assertNotEmpty(
$tracedReflection->getAttributes(Traceable::class),
"Method 'tracedMethod' should have Traceable attribute"
);
$this->assertEmpty(
$untracedReflection->getAttributes(Traceable::class),
"Method 'tracedMethod' should have Traceable attribute"
);
}

public function testMethodWithoutTraceableAttribute()
{
$reflection = new ReflectionMethod(NonTraceableClass::class, 'untracedMethod');
$attributes = $reflection->getAttributes(Traceable::class);

$this->assertEmpty($attributes, "Method 'untracedMethod' should not have Traceable attribute");
}
}
Loading

0 comments on commit d84a8a0

Please sign in to comment.