eerzho/opentelemetry-auto-class
Framework-agnostic automatic OpenTelemetry tracing for PHP 8.2+ using #[Traceable]. Mark a class and public methods generate spans via ext-opentelemetry hook API. Supports excluding methods; Laravel/Symfony integrations available.
Trace what your methods received, returned, and threw — without writing a single span. No framework required.
This is a read-only sub-split. Please open issues and pull requests in the monorepo.
The framework-agnostic engine — the Laravel and Symfony integrations build on it to discover and register your classes automatically.
composer require eerzho/opentelemetry-auto-class
Requirements:
Start with a plain service — we'll add tracing to it one attribute at a time:
namespace App\Service;
class OrderService
{
public function pay(int $orderId, string $card, Address $address): string {}
public function healthCheck(): bool {}
}
class Address
{
public function __construct(public string $city, public string $zip) {}
}
#[Trace] — mark a class for tracinguse Eerzho\Instrumentation\Class\Attribute\Trace;
#[Trace] // mark the class for tracing
class OrderService
{
public function pay(int $orderId, string $card, Address $address): string {}
public function healthCheck(): bool {}
}
#[TraceMethod] — trace a methoduse Eerzho\Instrumentation\Class\Attribute\TraceMethod;
#[TraceMethod(exclude: ['card'])] // capture every arg but card
// #[TraceMethod(include: ['orderId'])] // or: only orderId
public function pay(int $orderId, string $card, Address $address): string {}
A method gets a span with #[TraceMethod]. 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).
#[TraceProperties] — expand an object argumentuse Eerzho\Instrumentation\Class\Attribute\TraceProperties;
#[TraceProperties(exclude: ['zip'])] // expand every prop but zip
// #[TraceProperties(include: ['city'])] // or: only city
class Address
{
public function __construct(public string $city, public string $zip) {}
}
Once the attributes are in place, scan the classes and register the hooks — once, at bootstrap:
use Eerzho\Instrumentation\Class\AttributeScanner;
use Eerzho\Instrumentation\Class\ClassInstrumentation;
ClassInstrumentation::register(AttributeScanner::scan([OrderService::class]));
Skip AttributeScanner and hand register() the method map directly:
ClassInstrumentation::register([
OrderService::class => [
'pay' => [
'arguments' => ['orderId' => 0], // name => position
'return' => true,
'exception' => true,
],
'cancel' => ['arguments' => []], // trace, capture nothing
// methods not listed are not traced
],
]);
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.
include and exclude behave the same wherever they appear — #[TraceMethod] (arguments) and #[TraceProperties] (properties):
include |
exclude |
Result |
|---|---|---|
[] |
[] |
Everything (default) |
[a, b] |
[] |
Only a and b |
[] |
[a] |
Everything except a |
[a, b] |
[b] |
Only a — exclude wins on conflict |
An empty include means "no allowlist" (everything), not "nothing".
All properties are expanded — use exclude to drop sensitive ones (tokens, hashes).
Each captured argument is serialized to a span-compatible value:
| Type | Result |
|---|---|
string, int, float, bool |
As-is |
null |
"null" |
BackedEnum |
Backing value |
DateTimeInterface |
RFC3339 with milliseconds |
Object with #[TraceProperties] |
Expanded properties |
Object with __toString() |
String cast |
Object without #[TraceProperties] and __toString() |
Class name (FQCN) |
array |
First element sampled + array_count |
Other (resource, ...) |
gettype() result |
Object expansion via #[TraceProperties]:
code.argument.{name}.{property} (e.g. code.argument.address.city).#[TraceProperties]; otherwise it falls back to the rules above."uninitialized".__toString() failure falls back to the class name, so serialization never breaks the traced call.Array expansion:
code.argument.orders.0.id) — enough to see the shape without flooding the span.code.argument.{name}.array_count (e.g. code.argument.orders.array_count).Each traced call produces an INTERNAL span named ClassName::methodName, with:
| Attribute | Value |
|---|---|
code.function.name |
ClassName::methodName |
code.file.path |
File where the method is defined |
code.line.number |
Line number of the method |
code.return |
Return value, serialized the same way |
code.argument.{name} |
Method argument, keyed by parameter name, serialized the same way |
If the method throws, the span records an exception event and its status is set to ERROR:
| Event attribute | Value |
|---|---|
exception.type |
Exception class name |
exception.message |
Exception message |
exception.stacktrace |
Full stack trace |
With exception: false the status still becomes ERROR, but the event above is omitted.
AttributeScanner::scan() reflects over the classes you pass it:
#[Trace] on the class — no attribute, no instrumentation.#[TraceMethod], and for each the arguments, return value, and exception recording it configures.It returns a class → method → {arguments, return, exception} map. ClassInstrumentation::register() then installs an ext-opentelemetry hook on every mapped method:
code.* attributes and serialized arguments.ERROR status on failure, then ends the span.OTEL_PHP_DISABLED_INSTRUMENTATIONS=class
How can I help you explore Laravel packages today?