Observer API
The Observer API allows you to build profilers, tracers, and instrumentation tools that observe PHP function calls and errors. This is useful for:
- Performance profiling
- Request tracing (APM)
- Error monitoring
- Code coverage tools
- Debugging tools
Enabling the Feature
The Observer API is behind a feature flag. Add it to your Cargo.toml:
[dependencies]
ext-php-rs = { version = "0.15", features = ["observer"] }
Function Call Observer
Implement the FcallObserver trait to observe function calls:
use ext_php_rs::prelude::*;
use ext_php_rs::types::Zval;
use ext_php_rs::zend::ExecuteData;
use std::sync::atomic::{AtomicU64, Ordering};
struct CallCounter {
count: AtomicU64,
}
impl CallCounter {
fn new() -> Self {
Self {
count: AtomicU64::new(0),
}
}
}
impl FcallObserver for CallCounter {
fn should_observe(&self, info: &FcallInfo) -> bool {
!info.is_internal
}
fn begin(&self, _execute_data: &ExecuteData) {
self.count.fetch_add(1, Ordering::Relaxed);
}
fn end(&self, _execute_data: &ExecuteData, _retval: Option<&Zval>) {}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.fcall_observer(CallCounter::new)
}
The FcallObserver Trait
| Method | Description |
|---|---|
should_observe(&self, info: &FcallInfo) -> bool | Called once per function definition. Result is cached by PHP. |
begin(&self, execute_data: &ExecuteData) | Called when function begins execution. |
end(&self, execute_data: &ExecuteData, retval: Option<&Zval>) | Called when function ends (even on exceptions). |
FcallInfo - Function Metadata
| Field | Type | Description |
|---|---|---|
function_name | Option<&str> | Function name (None for anonymous/main) |
class_name | Option<&str> | Class name for methods |
filename | Option<&str> | Source file (None for internal functions) |
lineno | u32 | Line number (0 for internal functions) |
is_internal | bool | True for built-in PHP functions |
Error Observer
Implement the ErrorObserver trait to observe PHP errors:
use ext_php_rs::prelude::*;
use std::sync::atomic::{AtomicU64, Ordering};
struct ErrorTracker {
fatal_count: AtomicU64,
warning_count: AtomicU64,
}
impl ErrorTracker {
fn new() -> Self {
Self {
fatal_count: AtomicU64::new(0),
warning_count: AtomicU64::new(0),
}
}
}
impl ErrorObserver for ErrorTracker {
fn should_observe(&self, error_type: ErrorType) -> bool {
(ErrorType::FATAL | ErrorType::WARNING).contains(error_type)
}
fn on_error(&self, error: &ErrorInfo) {
if ErrorType::FATAL.contains(error.error_type) {
self.fatal_count.fetch_add(1, Ordering::Relaxed);
if let Some(trace) = error.backtrace() {
for frame in trace {
eprintln!(" at {}:{}",
frame.file.as_deref().unwrap_or("<internal>"),
frame.line
);
}
}
} else {
self.warning_count.fetch_add(1, Ordering::Relaxed);
}
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.error_observer(ErrorTracker::new)
}
The ErrorObserver Trait
| Method | Description |
|---|---|
should_observe(&self, error_type: ErrorType) -> bool | Filter which error types to observe. |
on_error(&self, error: &ErrorInfo) | Called when an observed error occurs. |
ErrorType - Error Level Bitflags
ErrorType::ERROR // E_ERROR
ErrorType::WARNING // E_WARNING
ErrorType::PARSE // E_PARSE
ErrorType::NOTICE // E_NOTICE
ErrorType::CORE_ERROR // E_CORE_ERROR
ErrorType::CORE_WARNING // E_CORE_WARNING
ErrorType::COMPILE_ERROR // E_COMPILE_ERROR
ErrorType::COMPILE_WARNING // E_COMPILE_WARNING
ErrorType::USER_ERROR // E_USER_ERROR
ErrorType::USER_WARNING // E_USER_WARNING
ErrorType::USER_NOTICE // E_USER_NOTICE
ErrorType::RECOVERABLE_ERROR // E_RECOVERABLE_ERROR
ErrorType::DEPRECATED // E_DEPRECATED
ErrorType::USER_DEPRECATED // E_USER_DEPRECATED
// Convenience groups
ErrorType::ALL // All error types
ErrorType::FATAL // ERROR | CORE_ERROR | COMPILE_ERROR | USER_ERROR | RECOVERABLE_ERROR | PARSE
ErrorType::CORE // CORE_ERROR | CORE_WARNING
ErrorInfo - Error Metadata
| Field | Type | Description |
|---|---|---|
error_type | ErrorType | The error level/severity |
filename | Option<&str> | Source file where error occurred |
lineno | u32 | Line number |
message | &str | The error message |
Lazy Backtrace
The backtrace() method captures the PHP call stack on demand:
fn on_error(&self, error: &ErrorInfo) {
if let Some(trace) = error.backtrace() {
for frame in trace {
println!("{}::{}() at {}:{}",
frame.class.as_deref().unwrap_or(""),
frame.function.as_deref().unwrap_or("<main>"),
frame.file.as_deref().unwrap_or("<internal>"),
frame.line
);
}
}
}
The backtrace is only captured when called, so there’s zero cost if unused.
Exception Observer
Implement the ExceptionObserver trait to observe thrown PHP exceptions:
use ext_php_rs::prelude::*;
use std::sync::atomic::{AtomicU64, Ordering};
struct ExceptionTracker {
exception_count: AtomicU64,
}
impl ExceptionTracker {
fn new() -> Self {
Self {
exception_count: AtomicU64::new(0),
}
}
}
impl ExceptionObserver for ExceptionTracker {
fn on_exception(&self, exception: &ExceptionInfo) {
self.exception_count.fetch_add(1, Ordering::Relaxed);
eprintln!("[EXCEPTION] {}: {} at {}:{}",
exception.class_name,
exception.message.as_deref().unwrap_or("<no message>"),
exception.file.as_deref().unwrap_or("<unknown>"),
exception.line
);
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.exception_observer(ExceptionTracker::new)
}
The ExceptionObserver Trait
| Method | Description |
|---|---|
on_exception(&self, exception: &ExceptionInfo) | Called when an exception is thrown, before any catch blocks. |
ExceptionInfo - Exception Metadata
| Field | Type | Description |
|---|---|---|
class_name | String | Exception class name (e.g., “RuntimeException”) |
message | Option<String> | The exception message |
code | i64 | The exception code |
file | Option<String> | Source file where thrown |
line | u32 | Line number where thrown |
Exception Backtrace
The backtrace() method captures the PHP call stack at exception throw time:
impl ExceptionObserver for MyObserver {
fn on_exception(&self, exception: &ExceptionInfo) {
eprintln!("[EXCEPTION] {}: {}",
exception.class_name,
exception.message.as_deref().unwrap_or("<no message>")
);
if let Some(trace) = exception.backtrace() {
for frame in trace {
eprintln!(" at {}::{}() in {}:{}",
frame.class.as_deref().unwrap_or(""),
frame.function.as_deref().unwrap_or("<main>"),
frame.file.as_deref().unwrap_or("<internal>"),
frame.line
);
}
}
}
}
The backtrace is lazy - only captured when called, so there’s zero cost if unused.
BacktraceFrame - Stack Frame Metadata
| Field | Type | Description |
|---|---|---|
function | Option<String> | Function name (None for main script) |
class | Option<String> | Class name for method calls |
file | Option<String> | Source file |
line | u32 | Line number |
Using All Observers
You can register all observers on the same module:
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.fcall_observer(MyProfiler::new)
.error_observer(MyErrorTracker::new)
.exception_observer(MyExceptionTracker::new)
}
Thread Safety
Observers are created once during MINIT and stored as global singletons.
They must implement Send + Sync because:
- NTS: A single instance handles all requests
- ZTS: The same instance may be called from different threads
Use thread-safe primitives like AtomicU64, Mutex, or RwLock for mutable state.
Best Practices
-
Keep observers lightweight: Observer methods are called frequently. Avoid heavy computations or I/O.
-
Use filtering wisely:
should_observeresults are cached for fcall observers. For error observers, filter early to avoid unnecessary processing. -
Handle errors gracefully: Don’t panic in observer methods.
-
Consider memory usage: Implement limits or periodic flushing to avoid unbounded memory growth.
-
Use lazy backtrace: Only call
backtrace()when needed. BothErrorInfoandExceptionInfosupport lazy backtrace capture.
Limitations
- Only one fcall observer can be registered per extension
- Only one error observer can be registered per extension
- Only one exception observer can be registered per extension
- Observers are registered globally for the entire PHP process