Skip to content

Commit de311c9

Browse files
authored
Add PHP Health Metrics (PHM) with /proc-based worker collection (#145)
1 parent 5a09c4b commit de311c9

14 files changed

Lines changed: 491 additions & 37 deletions

File tree

.github/workflows/rust.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,10 @@ jobs:
115115
- name: Setup php-fpm for Linux
116116
if: matrix.os == 'ubuntu-24.04'
117117
run: |
118-
sudo apt-get update
118+
sudo apt-get update
119+
sudo apt-get install -y software-properties-common
120+
sudo add-apt-repository -y ppa:ondrej/php
121+
sudo apt-get update
119122
sudo apt-get install -y php${{ matrix.flag.php_version }}-fpm
120123
sudo ln -sf /usr/sbin/php-fpm${{ matrix.flag.php_version }} /usr/sbin/php-fpm
121124
@@ -171,8 +174,18 @@ jobs:
171174
# Build mixture for cargo test.
172175
- name: Docker compose
173176
run: |
174-
docker compose up -d --wait
175-
docker compose ps
177+
for i in 1 2 3; do
178+
if docker compose up -d --wait && docker compose ps; then
179+
break
180+
fi
181+
echo "docker compose up failed (attempt ${i}/3), retrying in 10s..."
182+
docker compose down --remove-orphans 2>/dev/null || true
183+
if [ "${i}" -lt 3 ]; then
184+
sleep 10
185+
else
186+
exit 1
187+
fi
188+
done
176189
177190
# Try cargo test.
178191
- name: Cargo test

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ members = [
2121
]
2222

2323
[workspace.package]
24-
version = "1.1.0"
24+
version = "1.2.0"
2525
authors = ["Apache Software Foundation", "jmjoy <jmjoy@apache.org>", "Yanlong He <heyanlong@apache.org>"]
2626
edition = "2024"
2727
rust-version = "1.85"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
<img src="http://skywalking.apache.org/assets/logo.svg" alt="Sky Walking logo" height="90px" align="right" />
44

5-
**SkyWalking PHP** The PHP Agent for Apache SkyWalking, which provides the native tracing abilities for PHP project.
5+
**SkyWalking PHP** The PHP Agent for Apache SkyWalking, which provides native tracing and PHP Health Metrics (PHM)
6+
runtime reporting for PHP projects.
67

78
**SkyWalking** an APM(application performance monitor) system, especially designed for
89
microservices, cloud native and container-based (Docker, Kubernetes, Mesos) architectures.

docs/en/configuration/ini-settings.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ This is the configuration list supported in `php.ini`.
2727
| skywalking_agent.instance_name | Instance name. You can set `${HOSTNAME}`, refer to [Example #1](https://www.php.net/manual/en/install.fpm.configuration.php) | |
2828
| skywalking_agent.standalone_socket_path | Unix domain socket file path of standalone skywalking php worker. Only available when `reporter_type` is `standalone`. | |
2929
| skywalking_agent.psr_logging_level | The log level reported to SkyWalking, based on PSR-3, one of `Off`, `Debug`, `Info`, Notice`, Warning`, Error`, Critical`, Alert`, Emergency`. | Off |
30+
| skywalking_agent.metrics_enable | Enable PHP Health Metrics (PHM) meter reporting via native MeterReportService. **Linux only** (requires `/proc`). Default **On** on Linux when the agent is active; default **Off** on macOS/Windows. Set to `Off` to disable on Linux. Reports six process meters: CPU utilization, memory used/peak, virtual memory, thread count, and open FD count. See [PHP agent README](../setup/service-agent/php-agent/README.md#php-health-metrics-phm). | On (Linux); Off (other) |
31+
| skywalking_agent.metrics_report_period | PHM meter collection interval in seconds. Process meters are sampled by the forked reporter worker via `/proc` (parent PHP process PID). **Linux only.** | 30 |

docs/en/setup/service-agent/php-agent/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,48 @@ Refer to the Configuration section for more configuration items.
113113
> Enabling it by default will cause extra meaningless consumption when skywalking agent is not
114114
> needed (such as simply executing a php script).
115115
116+
### PHP Health Metrics (PHM)
117+
118+
> **Platform:** PHM process meters are **Linux only**. The forked reporter worker reads the
119+
> parent PHP process via `/proc` (`/proc/{pid}/status`, `stat`, and `fd`). They are not available
120+
> on macOS or Windows. Trace and other agent features are unchanged.
121+
122+
When `reporter_type` is `grpc` or `kafka`, the forked reporter worker boots
123+
`skywalking::metrics::Metricer` in `start_worker`, alongside heartbeat reporting. A background
124+
collector samples `/proc` for the parent PHP process (`getppid()`), updates Gauges, and `Metricer`
125+
reports meter data to OAP through the same path as traces and logs. PHM does not run when
126+
`reporter_type = standalone`.
127+
128+
PHM reports PHP runtime meters through the native Meter protocol (MeterReportService), without
129+
requiring HTTP traffic, similar to Python PVM and Ruby runtime meters.
130+
**PHM is enabled by default on Linux** when the agent is active (`skywalking_agent.enable = On`).
131+
To disable it or tune the interval, use `php.ini`:
132+
133+
```ini
134+
; Disable PHM if not needed (default is On on Linux).
135+
; skywalking_agent.metrics_enable = Off
136+
137+
; Report interval in seconds (default 30).
138+
skywalking_agent.metrics_report_period = 30
139+
```
140+
141+
PHM reports six process meters (aligned with OAP `php-runtime.yaml` and Horizon UI widgets):
142+
143+
| Agent meter name | OAP / UI expression | Source |
144+
| --- | --- | --- |
145+
| `instance_php_process_cpu_utilization` | `meter_instance_php_process_cpu_utilization` | `/proc/{pid}/stat` utime+stime delta |
146+
| `instance_php_memory_used_mb` | `meter_instance_php_memory_used_mb` | `/proc/{pid}/status` VmRSS |
147+
| `instance_php_memory_peak_mb` | `meter_instance_php_memory_peak_mb` | `/proc/{pid}/status` VmHWM |
148+
| `instance_php_virtual_memory_mb` | `meter_instance_php_virtual_memory_mb` | `/proc/{pid}/status` VmSize |
149+
| `instance_php_thread_count` | `meter_instance_php_thread_count` | `/proc/{pid}/status` Threads |
150+
| `instance_php_open_fd_count` | `meter_instance_php_open_fd_count` | `/proc/{pid}/fd` count |
151+
152+
On the OAP side, activate the `php-runtime` entry in
153+
`agent-analyzer.default.meterAnalyzerActiveFiles`. Horizon UI shows the widgets on the **General
154+
Service → Instance** dashboard when data is available.
155+
156+
See [INI Settings](../../../configuration/ini-settings.md) for all PHM options.
157+
116158
## Run
117159

118160
Start `php-fpm` server:

src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ const SKYWALKING_AGENT_STANDALONE_SOCKET_PATH: &str = "skywalking_agent.standalo
118118
/// `Info`, Notice`, Warning`, Error`, Critical`, Alert`, Emergency`.
119119
const SKYWALKING_AGENT_PSR_LOGGING_LEVEL: &str = "skywalking_agent.psr_logging_level";
120120

121+
/// Whether to report PHP Health Metrics (PHM) via native meter protocol.
122+
/// Default is **On on Linux** when the agent extension is active (`/proc`
123+
/// sampling only); **Off** on other platforms.
124+
const SKYWALKING_AGENT_METRICS_ENABLE: &str = "skywalking_agent.metrics_enable";
125+
126+
/// PHM report period in seconds. Meters are sampled at most once per period.
127+
const SKYWALKING_AGENT_METRICS_REPORT_PERIOD: &str = "skywalking_agent.metrics_report_period";
128+
121129
#[php_get_module]
122130
pub fn get_module() -> Module {
123131
let mut module = Module::new(
@@ -214,6 +222,15 @@ pub fn get_module() -> Module {
214222
"".to_string(),
215223
Policy::System,
216224
);
225+
#[cfg(target_os = "linux")]
226+
module.add_ini(SKYWALKING_AGENT_METRICS_ENABLE, true, Policy::System);
227+
#[cfg(not(target_os = "linux"))]
228+
module.add_ini(SKYWALKING_AGENT_METRICS_ENABLE, false, Policy::System);
229+
module.add_ini(
230+
SKYWALKING_AGENT_METRICS_REPORT_PERIOD,
231+
30i64,
232+
Policy::System,
233+
);
217234

218235
// Hooks.
219236
module.on_module_init(module::init);

src/module.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ pub static PSR_LOGGING_LEVEL: Lazy<PsrLogLevel> = Lazy::new(|| {
167167
.into()
168168
});
169169

170+
pub static METRICS_ENABLE: Lazy<bool> =
171+
Lazy::new(|| ini_get::<bool>(SKYWALKING_AGENT_METRICS_ENABLE));
172+
173+
pub static METRICS_REPORT_PERIOD: Lazy<i64> =
174+
Lazy::new(|| ini_get::<i64>(SKYWALKING_AGENT_METRICS_REPORT_PERIOD));
175+
170176
pub fn init() {
171177
if !is_enable() {
172178
return;
@@ -193,6 +199,8 @@ pub fn init() {
193199
Lazy::force(&KAFKA_PRODUCER_CONFIG);
194200
Lazy::force(&INJECT_CONTEXT);
195201
Lazy::force(&PSR_LOGGING_LEVEL);
202+
Lazy::force(&METRICS_ENABLE);
203+
Lazy::force(&METRICS_REPORT_PERIOD);
196204

197205
if let Err(err) = try_init_logger() {
198206
eprintln!("skywalking_agent: initialize logger failed: {}", err);
@@ -228,7 +236,6 @@ pub fn init() {
228236
return;
229237
}
230238

231-
// Initialize Agent worker.
232239
init_worker();
233240

234241
let reporter = Arc::new(Reporter::new(&*SOCKET_FILE_PATH));
@@ -239,7 +246,11 @@ pub fn init() {
239246
reporter.clone(),
240247
));
241248

242-
logger::set_global_logger(Logger::new(&*SERVICE_NAME, &*SERVICE_INSTANCE, reporter));
249+
logger::set_global_logger(Logger::new(
250+
&*SERVICE_NAME,
251+
&*SERVICE_INSTANCE,
252+
reporter.clone(),
253+
));
243254

244255
// Hook functions.
245256
register_execute_functions();

src/worker.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@
1414
// limitations under the License.
1515

1616
use crate::module::{
17-
AUTHENTICATION, ENABLE_TLS, HEARTBEAT_PERIOD, PROPERTIES_REPORT_PERIOD_FACTOR, REPORTER_TYPE,
18-
SERVER_ADDR, SERVICE_INSTANCE, SERVICE_NAME, SOCKET_FILE_PATH, SSL_CERT_CHAIN_PATH,
19-
SSL_KEY_PATH, SSL_TRUSTED_CA_PATH, WORKER_THREADS, is_standalone_reporter_type,
17+
AUTHENTICATION, ENABLE_TLS, HEARTBEAT_PERIOD, METRICS_ENABLE, METRICS_REPORT_PERIOD,
18+
PROPERTIES_REPORT_PERIOD_FACTOR, REPORTER_TYPE, SERVER_ADDR, SERVICE_INSTANCE, SERVICE_NAME,
19+
SOCKET_FILE_PATH, SSL_CERT_CHAIN_PATH, SSL_KEY_PATH, SSL_TRUSTED_CA_PATH, WORKER_THREADS,
20+
is_standalone_reporter_type,
2021
};
2122
#[cfg(feature = "kafka-reporter")]
2223
use crate::module::{KAFKA_BOOTSTRAP_SERVERS, KAFKA_PRODUCER_CONFIG};
2324
#[cfg(feature = "kafka-reporter")]
2425
use skywalking_php_worker::reporter::KafkaReporterConfiguration;
2526
use skywalking_php_worker::{
2627
HeartBeatConfiguration, WorkerConfiguration, new_tokio_runtime,
28+
phm::PhmConfiguration,
2729
reporter::{GrpcReporterConfiguration, ReporterConfiguration},
2830
start_worker,
2931
};
@@ -78,6 +80,7 @@ pub fn init_worker() {
7880
heartbeat_period: *HEARTBEAT_PERIOD,
7981
properties_report_period_factor: *PROPERTIES_REPORT_PERIOD_FACTOR,
8082
}),
83+
phm: phm_configuration(),
8184
reporter_config,
8285
};
8386

@@ -106,3 +109,20 @@ fn worker_threads() -> usize {
106109
worker_threads as usize
107110
}
108111
}
112+
113+
#[cfg(target_os = "linux")]
114+
fn phm_configuration() -> Option<PhmConfiguration> {
115+
if !*METRICS_ENABLE {
116+
return None;
117+
}
118+
Some(PhmConfiguration {
119+
service_name: SERVICE_NAME.clone(),
120+
service_instance: SERVICE_INSTANCE.clone(),
121+
report_period_secs: *METRICS_REPORT_PERIOD,
122+
})
123+
}
124+
125+
#[cfg(not(target_os = "linux"))]
126+
fn phm_configuration() -> Option<PhmConfiguration> {
127+
None
128+
}

tests/common/mod.rs

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,10 @@ use axum::{
2323
routing::any,
2424
};
2525
use futures_util::future::join_all;
26-
use libc::{SIGTERM, kill, pid_t};
26+
use libc::{SIGKILL, SIGTERM, kill, pid_t};
2727
use once_cell::sync::Lazy;
2828
use std::{
29-
env,
30-
fs::File,
31-
io::{self, Cursor},
32-
net::SocketAddr,
33-
process::{ExitStatus, Stdio},
34-
sync::Arc,
35-
thread,
36-
time::Duration,
29+
env, fs::File, io::Cursor, net::SocketAddr, process::Stdio, sync::Arc, thread, time::Duration,
3730
};
3831
use tokio::{
3932
net::TcpStream,
@@ -112,16 +105,13 @@ pub async fn teardown(fixture: Fixture) {
112105
fixture.http_server_1_handle.abort();
113106
fixture.http_server_2_handle.abort();
114107

115-
let results = join_all([
116-
kill_command(fixture.php_fpm_1_child),
117-
kill_command(fixture.php_fpm_2_child),
118-
kill_command(fixture.php_swoole_1_child),
119-
kill_command(fixture.php_swoole_2_child),
108+
join_all([
109+
stop_child(fixture.php_fpm_1_child),
110+
stop_child(fixture.php_fpm_2_child),
111+
stop_child(fixture.php_swoole_1_child),
112+
stop_child(fixture.php_swoole_2_child),
120113
])
121114
.await;
122-
for result in results {
123-
assert!(result.unwrap().success());
124-
}
125115
}
126116

127117
fn setup_logging() {
@@ -319,8 +309,22 @@ fn setup_php_fpm(index: usize, fpm_addr: &str) -> Child {
319309
"-d",
320310
"skywalking_agent.psr_logging_level=Warning",
321311
];
312+
let mut args: Vec<String> = args.iter().map(|s| (*s).to_string()).collect();
313+
if index == 1 {
314+
args.extend([
315+
"-d".to_owned(),
316+
"skywalking_agent.metrics_enable=On".to_owned(),
317+
"-d".to_owned(),
318+
"skywalking_agent.metrics_report_period=5".to_owned(),
319+
]);
320+
} else {
321+
args.extend([
322+
"-d".to_owned(),
323+
"skywalking_agent.metrics_enable=Off".to_owned(),
324+
]);
325+
}
322326
info!(cmd = args.join(" "), "start command");
323-
let child = Command::new(args[0])
327+
let child = Command::new(&args[0])
324328
.args(&args[1..])
325329
.stdin(Stdio::null())
326330
.stdout(File::create("/tmp/fpm-skywalking-stdout.log").unwrap())
@@ -364,6 +368,8 @@ fn setup_php_swoole(index: usize) -> Child {
364368
"skywalking_agent.enable_zend_observer={}",
365369
*ENABLE_ZEND_OBSERVER
366370
),
371+
"-d",
372+
"skywalking_agent.metrics_enable=Off",
367373
&format!("tests/php/swoole/main.{}.php", index),
368374
];
369375
info!(cmd = args.join(" "), "start command");
@@ -378,11 +384,21 @@ fn setup_php_swoole(index: usize) -> Child {
378384
child
379385
}
380386

381-
async fn kill_command(mut child: Child) -> io::Result<ExitStatus> {
382-
if let Some(id) = child.id() {
383-
unsafe {
384-
kill(id as pid_t, SIGTERM);
387+
async fn stop_child(mut child: Child) {
388+
let Some(id) = child.id() else {
389+
let _ = child.wait().await;
390+
return;
391+
};
392+
unsafe {
393+
kill(id as pid_t, SIGTERM);
394+
}
395+
match tokio::time::timeout(Duration::from_secs(5), child.wait()).await {
396+
Ok(Ok(_)) | Ok(Err(_)) => {}
397+
Err(_) => {
398+
unsafe {
399+
kill(id as pid_t, SIGKILL);
400+
}
401+
let _ = child.wait().await;
385402
}
386403
}
387-
child.wait().await
388404
}

0 commit comments

Comments
 (0)