Skip to content

Commit 948d82f

Browse files
authored
Trace method
* feat(core): add TraceMethod attribute with return and exception control Replace the argument-only TraceArguments attribute with a unified TraceMethod attribute that governs all method-level tracing from one place. Beyond capturing arguments, it can now record the return value as a span attribute and toggle exception recording independently, so callers can trace outcomes without leaking sensitive stack details. When exception recording is off, the span is still marked as errored without attaching the exception message. * refactor(core): replace TraceArguments with TraceMethod feat(core): capture return values and control exception recording Remove the deprecated TraceArguments attribute now that TraceMethod supersedes it, and migrate all fixtures, tests and benchmarks to the new attribute. The scan map now carries per-method arguments, return and exception flags instead of a bare argument-position map, so callers can toggle argument capture, return-value recording and exception details independently. Raise the phpbench memory_limit to 512M to keep class-generation benchmarks from exhausting memory. --- Header is two lines because the change is genuinely both a rename and a feature; collapse to just the `refactor` line if you want one type. → skipped: nothing. * refactor(docs): rename TraceArguments to TraceMethod Align the READMEs with the renamed attribute, which now captures the return value and records exceptions in addition to arguments. Document the new `arguments`/`return`/`exception` flags on both the attribute and the direct `register()` method map, plus the `code.return` span attribute and the exception-event opt-out. * feat(deps): pin internal package to self.version in bundles Ensure the Laravel and Symfony bundles always require the exact core package version from this monorepo instead of a loose ^0.3 constraint, keeping them in lockstep on every release.
1 parent dbde2b0 commit 948d82f

31 files changed

Lines changed: 355 additions & 136 deletions

benchmarks/ClassGenerator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function update(int \$id, string \$name, ?array \$options = null): void {
4848
public function delete(int \$id): void {}
4949
public function import(array \$items, bool \$force, ?\\DateTimeImmutable \$scheduledAt = null): void {}
5050
public function export(string \$format, array \$filters): string { return ''; }
51-
#[\\Eerzho\\Instrumentation\\Class\\Attribute\\TraceArguments(exclude: ['password', 'token'])]
51+
#[\\Eerzho\\Instrumentation\\Class\\Attribute\\TraceMethod(exclude: ['password', 'token'])]
5252
public function authenticate(string \$login, string \$password, string \$token): bool { return true; }
5353
}
5454
");

packages/core/README.md

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ namespace App\Service;
3030

3131
class OrderService
3232
{
33-
public function pay(int $orderId, string $card, Address $address): void {}
33+
public function pay(int $orderId, string $card, Address $address): string {}
3434
public function healthCheck(): bool {}
3535
}
3636

@@ -49,21 +49,23 @@ use Eerzho\Instrumentation\Class\Attribute\Trace;
4949
// #[Trace(include: ['pay'])] // or: only pay
5050
class OrderService
5151
{
52-
public function pay(int $orderId, string $card, Address $address): void {}
52+
public function pay(int $orderId, string $card, Address $address): string {}
5353
public function healthCheck(): bool {}
5454
}
5555
```
5656

57-
### 2. `#[TraceArguments]`pick the captured arguments
57+
### 2. `#[TraceMethod]`capture arguments, return value, and exceptions
5858

5959
```php
60-
use Eerzho\Instrumentation\Class\Attribute\TraceArguments;
60+
use Eerzho\Instrumentation\Class\Attribute\TraceMethod;
6161

62-
#[TraceArguments(exclude: ['card'])] // capture every arg but card
63-
// #[TraceArguments(include: ['orderId'])] // or: only orderId
64-
public function pay(int $orderId, string $card, Address $address): void {}
62+
#[TraceMethod(exclude: ['card'])] // capture every arg but card
63+
// #[TraceMethod(include: ['orderId'])] // or: only orderId
64+
public function pay(int $orderId, string $card, Address $address): string {}
6565
```
6666

67+
By default, it captures the arguments and the return value, and records exceptions. Turn each off with `arguments: false`, `return: false`, or `exception: false` (a disabled exception still sets the span status to `ERROR`, only its event is omitted).
68+
6769
### 3. `#[TraceProperties]` — expand an object argument
6870

6971
```php
@@ -95,20 +97,24 @@ Skip `AttributeScanner` and hand `register()` the method map directly:
9597
```php
9698
ClassInstrumentation::register([
9799
OrderService::class => [
98-
'pay' => ['orderId' => 0], // trace, capture orderId only
99-
'cancel' => [], // trace, capture nothing
100+
'pay' => [
101+
'arguments' => ['orderId' => 0], // name => position
102+
'return' => true,
103+
'exception' => true,
104+
],
105+
'cancel' => ['arguments' => []], // trace, capture nothing
100106
// methods not listed are not traced
101107
],
102108
]);
103109
```
104110

105-
Each entry maps a class → its methods → each method's arguments (`name => position`). Anything not listed is not traced or not captured.
111+
Each method maps to its `arguments` (`name => position`) plus the `return` and `exception` flags. Missing keys default to off. Anything not listed is not traced.
106112

107113
## Traced output
108114

109115
### Selecting what to trace
110116

111-
`include` and `exclude` behave the same wherever they appear — `#[Trace]`, `#[TraceArguments]`, `#[TraceProperties]`:
117+
`include` and `exclude` behave the same wherever they appear — `#[Trace]`, `#[TraceMethod]`, `#[TraceProperties]`:
112118

113119
| `include` | `exclude` | Result |
114120
|-----------|-----------|-------------------------------------------|
@@ -149,12 +155,13 @@ Object expansion via `#[TraceProperties]`:
149155

150156
Each traced call produces an `INTERNAL` span named `ClassName::methodName`, with:
151157

152-
| Attribute | Value |
153-
|----------------------|-----------------------------------|
154-
| `code.function.name` | `ClassName::methodName` |
155-
| `code.file.path` | File where the method is defined |
156-
| `code.line.number` | Line number of the method |
157-
| Method arguments | Parameter name → serialized value |
158+
| Attribute | Value |
159+
|----------------------|----------------------------------------------------------------------------|
160+
| `code.function.name` | `ClassName::methodName` |
161+
| `code.file.path` | File where the method is defined |
162+
| `code.line.number` | Line number of the method |
163+
| `code.return` | Return value, serialized the same way (on success, unless `return: false`) |
164+
| Method arguments | Parameter name → serialized value |
158165

159166
If the method throws, the span records an `exception` event and its status is set to `ERROR`:
160167

@@ -164,18 +171,20 @@ If the method throws, the span records an `exception` event and its status is se
164171
| `exception.message` | Exception message |
165172
| `exception.stacktrace` | Full stack trace |
166173

174+
With `exception: false` the status still becomes `ERROR`, but the event above is omitted.
175+
167176
## How it works
168177

169178
`AttributeScanner::scan()` reflects over the classes you pass it:
170179

171180
1. Skips abstract classes, interfaces, traits, and enums.
172181
2. Reads `#[Trace]` on the class — no attribute, no instrumentation.
173-
3. Collects the public methods allowed by `include` / `exclude`, and for each the arguments allowed by `#[TraceArguments]`.
182+
3. Collects the public methods allowed by `include` / `exclude`, and for each the arguments, return value, and exception recording configured by `#[TraceMethod]`.
174183

175-
It returns a `class → method → {argument: position}` map. `ClassInstrumentation::register()` then installs an `ext-opentelemetry` hook on every mapped method:
184+
It returns a `class → method → {arguments, return, exception}` map. `ClassInstrumentation::register()` then installs an `ext-opentelemetry` hook on every mapped method:
176185

177186
- **pre** — opens the span and attaches the `code.*` attributes and serialized arguments.
178-
- **post** — records the exception and sets `ERROR` status if the method threw, then ends the span.
187+
- **post**captures the return value on success, records the exception and sets `ERROR` status on failure, then ends the span.
179188

180189
## Disabling instrumentation
181190

packages/core/src/Attribute/TraceArguments.php renamed to packages/core/src/Attribute/TraceMethod.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
use Attribute;
88

99
#[Attribute(Attribute::TARGET_METHOD)]
10-
final readonly class TraceArguments
10+
final readonly class TraceMethod
1111
{
1212
/**
1313
* @param list<string> $include
1414
* @param list<string> $exclude
1515
*/
1616
public function __construct(
17+
public bool $arguments = true,
1718
public array $include = [],
1819
public array $exclude = [],
20+
public bool $return = true,
21+
public bool $exception = true,
1922
) {
2023
}
2124
}

packages/core/src/AttributeScanner.php

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace Eerzho\Instrumentation\Class;
66

77
use Eerzho\Instrumentation\Class\Attribute\Trace;
8-
use Eerzho\Instrumentation\Class\Attribute\TraceArguments;
8+
use Eerzho\Instrumentation\Class\Attribute\TraceMethod;
99
use ReflectionClass;
1010
use ReflectionException;
1111
use ReflectionMethod;
@@ -20,7 +20,7 @@ final class AttributeScanner
2020
*
2121
* @throws ReflectionException
2222
*
23-
* @return array<class-string, array<string, array<string, int>>>
23+
* @return array<class-string, array<string, array{arguments: array<string, int>, return: bool, exception: bool}>>
2424
*/
2525
public static function scan(array $classes): array
2626
{
@@ -41,7 +41,7 @@ public static function scan(array $classes): array
4141
*
4242
* @throws ReflectionException
4343
*
44-
* @return array<string, array<string, int>>
44+
* @return array<string, array{arguments: array<string, int>, return: bool, exception: bool}>
4545
*/
4646
private static function scanClass(string $class): array
4747
{
@@ -61,7 +61,7 @@ private static function scanClass(string $class): array
6161
/**
6262
* @param ReflectionClass<object> $class
6363
*
64-
* @return array<string, array<string, int>>
64+
* @return array<string, array{arguments: array<string, int>, return: bool, exception: bool}>
6565
*/
6666
private static function scanMethods(ReflectionClass $class): array
6767
{
@@ -73,32 +73,38 @@ private static function scanMethods(ReflectionClass $class): array
7373
$methodsMap = [];
7474
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
7575
if (self::isAllowed($method->getName(), $attribute->include, $attribute->exclude)) {
76-
$methodsMap[$method->getName()] = self::scanArguments($method);
76+
$methodsMap[$method->getName()] = self::scanMethod($method);
7777
}
7878
}
7979

8080
return $methodsMap;
8181
}
8282

8383
/**
84-
* @return array<string, int>
84+
* @return array{arguments: array<string, int>, return: bool, exception: bool}
8585
*/
86-
private static function scanArguments(ReflectionMethod $method): array
86+
private static function scanMethod(ReflectionMethod $method): array
8787
{
88-
$attribute = self::findTraceArguments($method);
88+
$attribute = self::findTraceMethod($method);
8989
if ($attribute === null) {
90-
return [];
90+
return ['arguments' => [], 'return' => false, 'exception' => false];
9191
}
9292

9393
$arguments = [];
94-
foreach ($method->getParameters() as $parameter) {
95-
$name = $parameter->getName();
96-
if (self::isAllowed($name, $attribute->include, $attribute->exclude)) {
97-
$arguments[$name] = $parameter->getPosition();
94+
if ($attribute->arguments) {
95+
foreach ($method->getParameters() as $parameter) {
96+
$name = $parameter->getName();
97+
if (self::isAllowed($name, $attribute->include, $attribute->exclude)) {
98+
$arguments[$name] = $parameter->getPosition();
99+
}
98100
}
99101
}
100102

101-
return $arguments;
103+
return [
104+
'arguments' => $arguments,
105+
'return' => $attribute->return,
106+
'exception' => $attribute->exception,
107+
];
102108
}
103109

104110
/**
@@ -127,12 +133,12 @@ private static function findTrace(ReflectionClass $class): ?Trace
127133
return $instance;
128134
}
129135

130-
private static function findTraceArguments(ReflectionMethod $method): ?TraceArguments
136+
private static function findTraceMethod(ReflectionMethod $method): ?TraceMethod
131137
{
132-
$attributes = $method->getAttributes(TraceArguments::class);
138+
$attributes = $method->getAttributes(TraceMethod::class);
133139
$instance = $attributes !== [] ? $attributes[0]->newInstance() : null;
134140

135-
assert($instance instanceof TraceArguments || $instance === null);
141+
assert($instance instanceof TraceMethod || $instance === null);
136142

137143
return $instance;
138144
}

packages/core/src/ClassInstrumentation.php

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class ClassInstrumentation
3737
public const NAME = 'class';
3838

3939
/**
40-
* @param array<class-string, array<string, array<string, int>>> $classesMap
40+
* @param array<class-string, array<string, array{arguments?: array<string, int>, return?: bool, exception?: bool}>> $classesMap
4141
*/
4242
public static function register(array $classesMap): void
4343
{
@@ -47,22 +47,24 @@ public static function register(array $classesMap): void
4747
);
4848

4949
foreach ($classesMap as $class => $methods) {
50-
foreach ($methods as $method => $arguments) {
51-
self::registerHook($instrumentation, $class, $method, $arguments);
50+
foreach ($methods as $method => $config) {
51+
self::registerHook($instrumentation, $class, $method, $config);
5252
}
5353
}
5454
}
5555

5656
/**
57-
* @param array<string, int> $arguments
57+
* @param array{arguments?: array<string, int>, return?: bool, exception?: bool} $config
5858
*/
5959
private static function registerHook(
6060
CachedInstrumentation $instrumentation,
6161
string $class,
6262
string $method,
63-
array $arguments,
63+
array $config,
6464
): void {
65-
$positionToName = array_flip($arguments);
65+
$positionToName = array_flip($config['arguments'] ?? []);
66+
$captureReturn = $config['return'] ?? false;
67+
$recordException = $config['exception'] ?? false;
6668

6769
hook(
6870
$class,
@@ -85,6 +87,7 @@ private static function registerHook(
8587
foreach ($positionToName as $position => $name) {
8688
if (array_key_exists($position, $params)) {
8789
foreach (self::serialize($name, $params[$position]) as $key => $value) {
90+
assert($key !== '');
8891
$builder->setAttribute($key, $value);
8992
}
9093
}
@@ -98,7 +101,7 @@ private static function registerHook(
98101
array $params,
99102
mixed $returnValue,
100103
?Throwable $exception,
101-
): void {
104+
) use ($captureReturn, $recordException): void {
102105
$scope = Context::storage()->scope();
103106
if ($scope === null) {
104107
return;
@@ -108,8 +111,17 @@ private static function registerHook(
108111
$span = Span::fromContext($scope->context());
109112

110113
if ($exception !== null) {
111-
$span->recordException($exception);
112-
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
114+
if ($recordException) {
115+
$span->recordException($exception);
116+
$span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
117+
} else {
118+
$span->setStatus(StatusCode::STATUS_ERROR);
119+
}
120+
} elseif ($captureReturn) {
121+
foreach (self::serialize('code.return', $returnValue) as $key => $value) {
122+
assert($key !== '');
123+
$span->setAttribute($key, $value);
124+
}
113125
}
114126

115127
$span->end();

packages/laravel/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ Add `#[Trace]` to any class in a scanned namespace:
4949
namespace App\Services;
5050

5151
use Eerzho\Instrumentation\Class\Attribute\Trace;
52-
use Eerzho\Instrumentation\Class\Attribute\TraceArguments;
52+
use Eerzho\Instrumentation\Class\Attribute\TraceMethod;
5353
use Eerzho\Instrumentation\Class\Attribute\TraceProperties;
5454

5555
#[Trace(exclude: ['healthCheck'])] // trace public methods, but hide "healthCheck"
5656
class OrderService
5757
{
5858
// span "App\Services\OrderService::pay"
59-
#[TraceArguments(exclude: ['card'])] // hide "card" from the span
59+
#[TraceMethod(exclude: ['card'])] // hide "card" from the span
6060
public function pay(int $orderId, string $card, Address $address): void {}
6161

6262
public function healthCheck(): bool {}

packages/laravel/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"php": ">=8.2",
3131
"ext-opentelemetry": "*",
3232
"open-telemetry/api": "^1.0",
33-
"eerzho/opentelemetry-auto-class": "^0.3",
33+
"eerzho/opentelemetry-auto-class": "self.version",
3434
"illuminate/support": "^11.0 || ^12.0 || ^13.0"
3535
},
3636
"autoload": {

packages/symfony/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ Add `#[Trace]` to any class registered as a service:
4040
namespace App\Service;
4141

4242
use Eerzho\Instrumentation\Class\Attribute\Trace;
43-
use Eerzho\Instrumentation\Class\Attribute\TraceArguments;
43+
use Eerzho\Instrumentation\Class\Attribute\TraceMethod;
4444
use Eerzho\Instrumentation\Class\Attribute\TraceProperties;
4545

4646
#[Trace(exclude: ['healthCheck'])] // trace public methods, but hide "healthCheck"
4747
class OrderService
4848
{
4949
// span "App\Service\OrderService::pay"
50-
#[TraceArguments(exclude: ['card'])] // hide "card" from the span
50+
#[TraceMethod(exclude: ['card'])] // hide "card" from the span
5151
public function pay(int $orderId, string $card, Address $address): void {}
5252

5353
public function healthCheck(): bool {}

packages/symfony/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"php": ">=8.2",
3131
"ext-opentelemetry": "*",
3232
"open-telemetry/api": "^1.0",
33-
"eerzho/opentelemetry-auto-class": "^0.3",
33+
"eerzho/opentelemetry-auto-class": "self.version",
3434
"symfony/http-kernel": "^6.0 || ^7.0 || ^8.0",
3535
"symfony/dependency-injection": "^6.0 || ^7.0 || ^8.0"
3636
},

packages/symfony/src/AutoClassBundle.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function boot(): void
6161
return;
6262
}
6363

64-
/** @var array<class-string, array<string, array<string, int>>> $classesMap */
64+
/** @var array<class-string, array<string, array{arguments?: array<string, int>, return?: bool, exception?: bool}>> $classesMap */
6565
$classesMap = $this->container->getParameter('otel.trace.classes_map');
6666
ClassInstrumentation::register($classesMap);
6767
}

0 commit comments

Comments
 (0)