This is an automated email from the ASF dual-hosted git repository.
jmjoy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-php.git
The following commit(s) were added to refs/heads/master by this push:
new 83ec158 Add php-amqp plugin (#146)
83ec158 is described below
commit 83ec158150f70ce6be770b127d7ee54990eaf4dc
Author: jmjoy <[email protected]>
AuthorDate: Fri Jun 26 15:51:14 2026 +0800
Add php-amqp plugin (#146)
---
.github/workflows/rust.yml | 4 +-
Cargo.lock | 20 ++--
Cargo.toml | 4 +-
README.md | 2 +-
dist-material/LICENSE | 10 +-
src/execute.rs | 4 +-
src/plugin/mod.rs | 2 +
src/plugin/plugin_amqp.rs | 203 +++++++++++++++++++++++++++++++++++++++
src/plugin/plugin_memcache.rs | 2 +-
src/plugin/plugin_mysqli.rs | 40 ++++----
src/plugin/plugin_psr3.rs | 18 +++-
src/plugin/style.rs | 16 +--
tests/data/expected_context.yaml | 94 +++++++++++++++++-
tests/e2e.rs | 9 ++
tests/php/fpm/amqp.php | 128 ++++++++++++++++++++++++
15 files changed, 499 insertions(+), 57 deletions(-)
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index d92b69c..c4fb562 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -95,7 +95,7 @@ jobs:
- name: Install dependencies for Linux
if: matrix.os == 'ubuntu-24.04'
- run: sudo apt-get update && sudo apt-get install -y llvm-18-dev
libclang-18-dev protobuf-compiler libsasl2-dev
+ run: sudo apt-get update && sudo apt-get install -y llvm-18-dev
libclang-18-dev protobuf-compiler libsasl2-dev librabbitmq-dev
- name: Install protobuf for Macos
if: matrix.os == 'macos-14'
@@ -110,7 +110,7 @@ jobs:
bcmath, calendar, ctype, dom, exif, gettext, iconv, intl, json,
mbstring,
mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline,
redis,
memcached, swoole-${{ matrix.flag.swoole_version }}, xml,
xmlreader, xmlwriter,
- yaml, zip, mongodb, memcache
+ yaml, zip, mongodb, memcache, amqp
- name: Setup php-fpm for Linux
if: matrix.os == 'ubuntu-24.04'
diff --git a/Cargo.lock b/Cargo.lock
index 5f6ac84..cb6a6f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1695,9 +1695,9 @@ dependencies = [
[[package]]
name = "phper"
-version = "0.17.2"
+version = "0.17.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97a3055aa0aec518814199bf9d114eeec267ec89b5008b65c2453ce2cd3ab993"
+checksum = "5a6ded58ef5936b049dcf4b73307889a21b49b9ebd51069f0a554c75275064a7"
dependencies = [
"cfg-if",
"derive_more",
@@ -1712,9 +1712,9 @@ dependencies = [
[[package]]
name = "phper-alloc"
-version = "0.16.1"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a188ae2a830b7552466093a9acc24e620a6a8678f0bb00384eac8e05cba66c8"
+checksum = "717915904f6ee06640b3232d8d409d7cf3d55b8fc878c83fbd78f73a10466edb"
dependencies = [
"phper-build",
"phper-sys",
@@ -1722,18 +1722,18 @@ dependencies = [
[[package]]
name = "phper-build"
-version = "0.15.4"
+version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3158c29309413a44b2bb80c17d0da09b53df071284f0c4687a232f0787f56a38"
+checksum = "6996befb91ef0538a35dd59f712c9667ad52905750bff74059886e1061a899e2"
dependencies = [
"phper-sys",
]
[[package]]
name = "phper-macros"
-version = "0.15.2"
+version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dc718c423b154a612117abcc3f0978a8ce9e295d6a80dba6b1b3a7fad3e0ddf"
+checksum = "82de7b26c685b446a2f0e2e718aa36b198cf806b094b7316d04b55b950c0a56a"
dependencies = [
"proc-macro2",
"quote",
@@ -1742,9 +1742,9 @@ dependencies = [
[[package]]
name = "phper-sys"
-version = "0.15.4"
+version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "758c37b62c9aa4e4aaf1a04ffdd7631b886fc7086db651fe3d4c0097bc101428"
+checksum = "cd0c78f99806ddfa651f0e1fc7aa86ab616a1819fb8335067f4cc2fb52f7c881"
dependencies = [
"bindgen",
"cc",
diff --git a/Cargo.toml b/Cargo.toml
index b5a6c0a..6c646e7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -60,7 +60,7 @@ futures-util = "0.3.31"
hostname = "0.4.1"
libc = "0.2.174"
once_cell = "1.21.3"
-phper = "0.17.2"
+phper = "0.17.5"
prost = "0.14.1"
rdkafka = { workspace = true, optional = true }
skywalking = { version = "0.10.0", features = ["management"] }
@@ -81,7 +81,7 @@ fastcgi-client = "0.9.0"
reqwest = { version = "0.12.22", features = ["trust-dns", "json", "stream"] }
[build-dependencies]
-phper-build = "0.15.3"
+phper-build = "0.15.5"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(phper_major_version,
values("8"))'] }
diff --git a/README.md b/README.md
index 60c8415..7fb66bb 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ SkyWalking PHP Agent requires SkyWalking 8.4+ and PHP 7.2+
* [x] [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php)
* [x] [Memcached](https://www.php.net/manual/en/book.memcached.php)
* [x] [phpredis](https://github.com/phpredis/phpredis)
- * [ ] [php-amqp](https://github.com/php-amqp/php-amqp)
+ * [x] [php-amqp](https://github.com/php-amqp/php-amqp) for Message Queuing
Producer
* [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka)
* [x] [predis](https://github.com/predis/predis)
* [x] [php-amqplib](https://github.com/php-amqplib/php-amqplib) for Message
Queuing Producer
diff --git a/dist-material/LICENSE b/dist-material/LICENSE
index 33ed232..89cfb69 100644
--- a/dist-material/LICENSE
+++ b/dist-material/LICENSE
@@ -654,11 +654,11 @@ MulanPSL-2.0 licenses
The following components are provided under the MulanPSL-2.0 License. See
project link for details.
The text of each license is also included in licenses/LICENSE-[project].txt.
- https://crates.io/crates/phper/0.17.2 0.17.2 MulanPSL-2.0
- https://crates.io/crates/phper-alloc/0.16.1 0.16.1 MulanPSL-2.0
- https://crates.io/crates/phper-build/0.15.4 0.15.4 MulanPSL-2.0
- https://crates.io/crates/phper-macros/0.15.2 0.15.2 MulanPSL-2.0
- https://crates.io/crates/phper-sys/0.15.4 0.15.4 MulanPSL-2.0
+ https://crates.io/crates/phper/0.17.5 0.17.5 MulanPSL-2.0
+ https://crates.io/crates/phper-alloc/0.16.3 0.16.3 MulanPSL-2.0
+ https://crates.io/crates/phper-build/0.15.6 0.15.6 MulanPSL-2.0
+ https://crates.io/crates/phper-macros/0.15.3 0.15.3 MulanPSL-2.0
+ https://crates.io/crates/phper-sys/0.15.6 0.15.6 MulanPSL-2.0
========================================================================
Unicode-3.0 licenses
diff --git a/src/execute.rs b/src/execute.rs
index 04c7449..9b1cf23 100644
--- a/src/execute.rs
+++ b/src/execute.rs
@@ -321,8 +321,8 @@ fn infer_request_id(execute_data: &mut ExecuteData) ->
Option<i64> {
prev_execute_data_ptr = unsafe {
(*prev_execute_data_ptr).prev_execute_data };
continue;
}
- let request = prev_execute_data.get_mut_parameter(0).as_mut_z_obj()?;
- match request.get_mut_property("fd").as_long() {
+ let request = prev_execute_data.get_parameter(0).as_z_obj()?;
+ match request.get_property("fd").as_long() {
Some(fd) => return Some(fd),
None => {
error!("infer request id failed");
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index 4f7623e..9f97e22 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -13,6 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+mod plugin_amqp;
mod plugin_amqplib;
mod plugin_curl;
mod plugin_memcache;
@@ -48,6 +49,7 @@ static PLUGINS: Lazy<Vec<Box<DynPlugin>>> = Lazy::new(|| {
Box::<plugin_predis::PredisPlugin>::default(),
Box::<plugin_memcached::MemcachedPlugin>::default(),
Box::<plugin_redis::RedisPlugin>::default(),
+ Box::<plugin_amqp::AmqpPlugin>::default(),
Box::<plugin_amqplib::AmqplibPlugin>::default(),
Box::<plugin_mongodb::MongodbPlugin>::default(),
Box::<plugin_memcache::MemcachePlugin>::default(),
diff --git a/src/plugin/plugin_amqp.rs b/src/plugin/plugin_amqp.rs
new file mode 100644
index 0000000..61cbced
--- /dev/null
+++ b/src/plugin/plugin_amqp.rs
@@ -0,0 +1,203 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::{Plugin, log_exception};
+use crate::{
+ component::COMPONENT_AMQP_PRODUCER_ID,
+ context::{RequestContext, SW_HEADER},
+ execute::{AfterExecuteHook, BeforeExecuteHook, get_this_mut,
validate_num_args},
+ tag::{TAG_MQ_BROKER, TAG_MQ_QUEUE, TAG_MQ_TOPIC},
+};
+use phper::{
+ arrays::ZArray,
+ objects::ZObj,
+ values::{ExecuteData, ZVal},
+};
+use skywalking::{
+ proto::v3::SpanLayer,
+ trace::span::{HandleSpanObject, Span},
+};
+
+#[derive(Default, Clone)]
+pub struct AmqpPlugin;
+
+impl Plugin for AmqpPlugin {
+ fn class_names(&self) -> Option<&'static [&'static str]> {
+ Some(&["AMQPExchange"])
+ }
+
+ fn function_name_prefix(&self) -> Option<&'static str> {
+ None
+ }
+
+ fn hook(
+ &self, class_name: Option<&str>, function_name: &str,
+ ) -> Option<(
+ Box<crate::execute::BeforeExecuteHook>,
+ Box<crate::execute::AfterExecuteHook>,
+ )> {
+ match (class_name, function_name) {
+ (Some(class_name @ "AMQPExchange"), function_name @ "publish") => {
+ Some(self.hook_exchange_publish(class_name, function_name))
+ }
+ _ => None,
+ }
+ }
+}
+
+impl AmqpPlugin {
+ fn hook_exchange_publish(
+ &self, class_name: &str, function_name: &str,
+ ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+ let class_name = class_name.to_owned();
+ let function_name = function_name.to_owned();
+ (
+ Box::new(move |request_id, execute_data| {
+ validate_num_args(execute_data, 1)?;
+
+ let this = get_this_mut(execute_data)?;
+
+ let peer = Self::get_peer(this)?;
+
+ let exchange = this
+ .call("getName", [])
+ .ok()
+ .and_then(|v| {
+ v.as_z_str()
+ .and_then(|s| s.to_str().ok())
+ .map(ToOwned::to_owned)
+ })
+ .unwrap_or_default();
+
+ let routing_key = if execute_data.num_args() >= 2 {
+ execute_data
+ .get_parameter(1)
+ .as_z_str()
+ .and_then(|s| s.to_str().ok())
+ .map(ToOwned::to_owned)
+ .unwrap_or_default()
+ } else {
+ String::new()
+ };
+
+ let span = Self::create_exit_span(
+ request_id,
+ &class_name,
+ &function_name,
+ &peer,
+ &exchange,
+ &routing_key,
+ )?;
+
+ Self::inject_sw_header(request_id, execute_data, &peer)?;
+
+ Ok(Box::new(span))
+ }),
+ Box::new(move |_, span, _, _| {
+ let mut span = span.downcast::<Span>().unwrap();
+ log_exception(&mut *span);
+ Ok(())
+ }),
+ )
+ }
+
+ fn get_peer(this: &mut ZObj) -> crate::Result<String> {
+ let mut channel = this.call("getChannel", [])?;
+ let channel = channel
+ .expect_mut_z_obj()
+ .map_err(|e| anyhow::anyhow!("channel isn't object: {}", e))?;
+ let mut connection = channel.call("getConnection", [])?;
+ let connection = connection
+ .expect_mut_z_obj()
+ .map_err(|e| anyhow::anyhow!("connection isn't object: {}", e))?;
+ let host = connection.call("getHost", [])?;
+ let host = host
+ .expect_z_str()
+ .map_err(|e| anyhow::anyhow!("host isn't string: {}", e))?
+ .to_str()?;
+ let port = connection.call("getPort", [])?;
+ let port = port.as_long().unwrap_or_default();
+ Ok(format!("{}:{}", host, port))
+ }
+
+ fn create_exit_span(
+ request_id: Option<i64>, class_name: &str, function_name: &str, peer:
&str, exchange: &str,
+ routing_key: &str,
+ ) -> crate::Result<Span> {
+ let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
+ Ok(ctx.create_exit_span(&format!("{}->{}", class_name,
function_name), peer))
+ })?;
+
+ let span_object = span.span_object_mut();
+ span_object.set_span_layer(SpanLayer::Mq);
+ span_object.component_id = COMPONENT_AMQP_PRODUCER_ID;
+ span_object.add_tag(TAG_MQ_BROKER, peer);
+ span_object.add_tag(TAG_MQ_TOPIC, exchange);
+ span_object.add_tag(TAG_MQ_QUEUE, routing_key);
+
+ Ok(span)
+ }
+
+ fn inject_sw_header(
+ request_id: Option<i64>, execute_data: &mut ExecuteData, peer: &str,
+ ) -> crate::Result<()> {
+ let sw_header = RequestContext::try_get_sw_header(request_id, peer)?;
+
+ let attributes = Self::ensure_attributes(execute_data)?;
+ let headers = Self::ensure_headers(attributes)?;
+ headers.insert(SW_HEADER, sw_header);
+
+ Ok(())
+ }
+
+ fn ensure_attributes(
+ execute_data: &mut ExecuteData,
+ ) -> crate::Result<&mut phper::arrays::ZArr> {
+ // php-amqp parses publish() as `s|s!l!a/`, so the 4th `headers`
+ // argument is only visible after the missing optional parameters are
+ // materialized and `num_args` reflects that.
+ execute_data.materialize_missing([
+ ZVal::from(()),
+ ZVal::from(()),
+ ZVal::from(ZArray::new()),
+ ])?;
+
+ let attributes = execute_data.get_mut_parameter(3);
+ if attributes.as_z_arr().is_none() {
+ *attributes = ZArray::new().into();
+ }
+
+ Ok(attributes
+ .as_mut_z_arr()
+ .ok_or_else(|| anyhow::anyhow!("attributes isn't array"))?)
+ }
+
+ fn ensure_headers(
+ attributes: &mut phper::arrays::ZArr,
+ ) -> crate::Result<&mut phper::arrays::ZArr> {
+ let has_headers = attributes
+ .get("headers")
+ .and_then(|headers| headers.as_z_arr())
+ .is_some();
+ if !has_headers {
+ attributes.insert("headers", ZArray::new());
+ }
+
+ Ok(attributes
+ .get_mut("headers")
+ .and_then(|headers| headers.as_mut_z_arr())
+ .ok_or_else(|| anyhow::anyhow!("headers isn't array"))?)
+ }
+}
diff --git a/src/plugin/plugin_memcache.rs b/src/plugin/plugin_memcache.rs
index 2b6fb6c..027131d 100644
--- a/src/plugin/plugin_memcache.rs
+++ b/src/plugin/plugin_memcache.rs
@@ -200,7 +200,7 @@ impl MemcachePlugin {
.unwrap();
let key = style
- .get_mut_parameter(execute_data, 0)
+ .get_parameter(execute_data, 0)
.as_z_str()
.and_then(|s| s.to_str().ok())
.map(ToOwned::to_owned)
diff --git a/src/plugin/plugin_mysqli.rs b/src/plugin/plugin_mysqli.rs
index 6e5c261..71b3b5d 100644
--- a/src/plugin/plugin_mysqli.rs
+++ b/src/plugin/plugin_mysqli.rs
@@ -94,7 +94,8 @@ impl MySQLImprovedPlugin {
Box::new(move |request_id, execute_data| {
// Sometimes the connection is failed. Therefore, first
assemble the peer from
// the parameters to prevent assembly failure in the after
hook.
- let peer = get_peer_by_parameters(execute_data, style);
+ let peer =
+ get_peer_by_parameters(execute_data,
class_name.as_deref(), &function_name);
let span = create_mysqli_exit_span(
request_id,
@@ -159,10 +160,8 @@ impl MySQLImprovedPlugin {
style,
)?;
- if execute_data.num_args() >= 1 {
- if let Some(statement) =
execute_data.get_parameter(0).as_z_str() {
- span.add_tag("db.statement", statement.to_str()?);
- }
+ if let Some(statement) =
execute_data.get_parameter(0).as_z_str() {
+ span.add_tag("db.statement", statement.to_str()?);
}
Ok(Box::new(span) as _)
@@ -226,21 +225,28 @@ fn get_peer_by_this(this: &mut ZObj) -> Option<String> {
})
}
-fn get_peer_by_parameters(execute_data: &mut ExecuteData, style: ApiStyle) ->
String {
- let mut peer = "".to_owned();
+fn get_peer_by_parameters(
+ execute_data: &ExecuteData, class_name: Option<&str>, function_name: &str,
+) -> String {
+ let (host_index, port_index) = match (class_name, function_name) {
+ (Some("mysqli"), "__construct" | "real_connect") => (0, 4),
+ (None, "mysqli_connect") => (0, 4),
+ (None, "mysqli_real_connect") => (1, 5),
+ _ => return String::new(),
+ };
- if style.validate_num_args(execute_data, 1).is_ok() {
- peer.push_str(
- style
- .get_mut_parameter(execute_data, 0)
- .as_z_str()
- .and_then(|s| s.to_str().ok())
- .unwrap_or_default(),
- );
- }
+ let mut peer = String::new();
+
+ peer.push_str(
+ execute_data
+ .get_parameter(host_index)
+ .as_z_str()
+ .and_then(|s| s.to_str().ok())
+ .unwrap_or_default(),
+ );
if !peer.is_empty() {
- let port = style.get_mut_parameter(execute_data, 4);
+ let port = execute_data.get_parameter(port_index);
#[allow(clippy::manual_map)]
let port = if let Some(port) = port.as_z_str() {
diff --git a/src/plugin/plugin_psr3.rs b/src/plugin/plugin_psr3.rs
index 9e4a2c7..28088c1 100644
--- a/src/plugin/plugin_psr3.rs
+++ b/src/plugin/plugin_psr3.rs
@@ -16,7 +16,7 @@
use super::Plugin;
use crate::{
context::RequestContext,
- execute::{AfterExecuteHook, BeforeExecuteHook, Noop},
+ execute::{AfterExecuteHook, BeforeExecuteHook, Noop, validate_num_args},
log::PsrLogLevel,
module::PSR_LOGGING_LEVEL,
};
@@ -85,8 +85,14 @@ impl Psr3Plugin {
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(move |request_id, execute_data| {
+ validate_num_args(execute_data, 1)?;
+
let message =
Self::handle_message(execute_data.get_mut_parameter(0))?;
- let context =
Self::handle_context(execute_data.get_mut_parameter(1))?;
+ let context = if execute_data.num_args() >= 2 {
+ Self::handle_context(execute_data.get_mut_parameter(1))?
+ } else {
+ vec![]
+ };
Self::handle_log(
class_name.clone(),
function_name.clone(),
@@ -107,13 +113,19 @@ impl Psr3Plugin {
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(move |request_id, execute_data| {
+ validate_num_args(execute_data, 2)?;
+
let log_level =
execute_data.get_parameter(0).expect_z_str()?.to_str()?;
let log_level: PsrLogLevel = log_level.into();
if log_level < *PSR_LOGGING_LEVEL {
return Ok(Box::new(()));
}
let message =
Self::handle_message(execute_data.get_mut_parameter(1))?;
- let context =
Self::handle_context(execute_data.get_mut_parameter(2))?;
+ let context = if execute_data.num_args() >= 3 {
+ Self::handle_context(execute_data.get_mut_parameter(2))?
+ } else {
+ vec![]
+ };
Self::handle_log(
class_name.clone(),
function_name.clone(),
diff --git a/src/plugin/style.rs b/src/plugin/style.rs
index 4d99bf6..4258372 100644
--- a/src/plugin/style.rs
+++ b/src/plugin/style.rs
@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use crate::execute::{get_this_mut, validate_num_args};
+use crate::execute::get_this_mut;
use anyhow::Context;
use phper::{
objects::ZObj,
@@ -40,22 +40,12 @@ impl ApiStyle {
}
}
- pub fn get_mut_parameter(self, execute_data: &mut ExecuteData, index:
usize) -> &mut ZVal {
+ pub fn get_parameter(self, execute_data: &ExecuteData, index: usize) ->
&ZVal {
let index = match self {
ApiStyle::OO => index,
ApiStyle::Procedural => index + 1,
};
- execute_data.get_mut_parameter(index)
- }
-
- pub fn validate_num_args(
- self, execute_data: &mut ExecuteData, num: usize,
- ) -> anyhow::Result<()> {
- let num = match self {
- ApiStyle::OO => num,
- ApiStyle::Procedural => num + 1,
- };
- validate_num_args(execute_data, num)
+ execute_data.get_parameter(index)
}
pub fn generate_operation_name(self, class_name: Option<&str>,
function_name: &str) -> String {
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index 40fbc20..4882270 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -15,7 +15,7 @@
segmentItems:
- serviceName: skywalking-agent-test-1
- segmentSize: 20
+ segmentSize: 21
segments:
- segmentId: "not null"
spans:
@@ -1209,6 +1209,98 @@ segmentItems:
- { key: url, value: "http://127.0.0.1:9011/rabbitmq.php" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
+ - segmentId: "not null"
+ spans:
+ - operationName: AMQPExchange->publish
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: MQ
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 144
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:5672
+ skipAnalysis: false
+ tags:
+ - { key: mq.broker, value: "127.0.0.1:5672" }
+ - { key: mq.topic, value: fanout_exchange_test }
+ - { key: mq.queue, value: "" }
+ - operationName: AMQPExchange->publish
+ parentSpanId: 0
+ spanId: 2
+ spanLayer: MQ
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 144
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:5672
+ skipAnalysis: false
+ tags:
+ - { key: mq.broker, value: "127.0.0.1:5672" }
+ - { key: mq.topic, value: "" }
+ - { key: mq.queue, value: queue_test }
+ - operationName: AMQPExchange->publish
+ parentSpanId: 0
+ spanId: 3
+ spanLayer: MQ
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 144
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:5672
+ skipAnalysis: false
+ tags:
+ - { key: mq.broker, value: "127.0.0.1:5672" }
+ - { key: mq.topic, value: exchange_test }
+ - { key: mq.queue, value: routing_test }
+ - operationName: AMQPExchange->publish
+ parentSpanId: 0
+ spanId: 4
+ spanLayer: MQ
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 144
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:5672
+ skipAnalysis: false
+ tags:
+ - { key: mq.broker, value: "127.0.0.1:5672" }
+ - { key: mq.topic, value: "" }
+ - { key: mq.queue, value: queue_test }
+ - operationName: AMQPExchange->publish
+ parentSpanId: 0
+ spanId: 5
+ spanLayer: MQ
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 144
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:5672
+ skipAnalysis: false
+ tags:
+ - { key: mq.broker, value: "127.0.0.1:5672" }
+ - { key: mq.topic, value: "" }
+ - { key: mq.queue, value: queue_test }
+ - operationName: GET:/amqp.php
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 8001
+ isError: false
+ spanType: Entry
+ peer: ""
+ skipAnalysis: false
+ tags:
+ - { key: url, value: "http://127.0.0.1:9011/amqp.php" }
+ - { key: http.method, value: GET }
+ - { key: http.status_code, value: "200" }
- segmentId: "not null"
spans:
- operationName: "MongoDB\\Driver\\Manager->executeCommand"
diff --git a/tests/e2e.rs b/tests/e2e.rs
index a76ce6c..7b325c6 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -62,6 +62,7 @@ async fn run_e2e() {
request_fpm_memcached().await;
request_fpm_redis().await;
request_fpm_rabbitmq().await;
+ request_fpm_amqp().await;
request_fpm_mongodb().await;
request_fpm_memcache().await;
request_fpm_monolog().await;
@@ -152,6 +153,14 @@ async fn request_fpm_rabbitmq() {
.await;
}
+async fn request_fpm_amqp() {
+ request_common(
+ HTTP_CLIENT.get(format!("http://{}/amqp.php", PROXY_SERVER_1_ADDRESS)),
+ "ok",
+ )
+ .await;
+}
+
async fn request_fpm_mongodb() {
request_common(
HTTP_CLIENT.get(format!("http://{}/mongodb.php",
PROXY_SERVER_1_ADDRESS)),
diff --git a/tests/php/fpm/amqp.php b/tests/php/fpm/amqp.php
new file mode 100644
index 0000000..7c30608
--- /dev/null
+++ b/tests/php/fpm/amqp.php
@@ -0,0 +1,128 @@
+<?php
+
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+function assertTrue(bool $condition, string $message): void
+{
+ if (!$condition) {
+ throw new RuntimeException($message);
+ }
+}
+
+function assertSameValue($expected, $actual, string $message): void
+{
+ if ($expected !== $actual) {
+ throw new RuntimeException(sprintf('%s. expected=%s actual=%s',
$message, var_export($expected, true), var_export($actual, true)));
+ }
+}
+
+function expectEnvelope(AMQPQueue $queue, string $body, callable $assertions):
void
+{
+ $envelope = null;
+ for ($attempt = 0; $attempt < 10; $attempt++) {
+ $envelope = $queue->get(AMQP_AUTOACK);
+ if ($envelope instanceof AMQPEnvelope) {
+ break;
+ }
+ usleep(100000);
+ }
+
+ assertTrue($envelope instanceof AMQPEnvelope, 'expected message in queue');
+ assertSameValue($body, $envelope->getBody(), 'unexpected message body');
+ $assertions($envelope);
+}
+
+
+$connection = new AMQPConnection(['host' => '127.0.0.1', 'port' => 5672,
'login' => 'guest', 'password' => 'guest']);
+$connection->connect();
+$channel = new AMQPChannel($connection);
+
+$queue = new AMQPQueue($channel);
+$queue->setName('queue_test');
+$queue->setFlags(AMQP_NOPARAM);
+$queue->declareQueue();
+
+$exchange = new AMQPExchange($channel);
+$exchange->setName('exchange_test');
+$exchange->setType(AMQP_EX_TYPE_DIRECT);
+$exchange->declareExchange();
+
+$fanoutQueue = new AMQPQueue($channel);
+$fanoutQueue->setName('fanout_queue_test');
+$fanoutQueue->setFlags(AMQP_NOPARAM);
+$fanoutQueue->declareQueue();
+
+$fanoutExchange = new AMQPExchange($channel);
+$fanoutExchange->setName('fanout_exchange_test');
+$fanoutExchange->setType(AMQP_EX_TYPE_FANOUT);
+$fanoutExchange->declareExchange();
+
+$queue->bind('exchange_test', 'routing_test');
+$fanoutQueue->bind('fanout_exchange_test');
+
+$queue->purge();
+$fanoutQueue->purge();
+
+{
+ $exchange = new AMQPExchange($channel);
+ $exchange->setName('fanout_exchange_test');
+ $exchange->publish('One Arg Message');
+
+ expectEnvelope($fanoutQueue, 'One Arg Message', static function
(AMQPEnvelope $envelope): void {
+ assertTrue($envelope->hasHeader('sw8'), 'missing sw8 header for
one-arg publish');
+ });
+}
+
+{
+ $exchange = new AMQPExchange($channel);
+ $exchange->publish('Two Arg Message', 'queue_test');
+
+ expectEnvelope($queue, 'Two Arg Message', static function (AMQPEnvelope
$envelope): void {
+ assertTrue($envelope->hasHeader('sw8'), 'missing sw8 header for
two-arg publish');
+ });
+}
+
+{
+ $exchange = new AMQPExchange($channel);
+ $exchange->setName('exchange_test');
+ $exchange->publish('Three Arg Message', 'routing_test', AMQP_NOPARAM);
+
+ expectEnvelope($queue, 'Three Arg Message', static function (AMQPEnvelope
$envelope): void {
+ assertTrue($envelope->hasHeader('sw8'), 'missing sw8 header for
three-arg publish');
+ });
+}
+
+{
+ $exchange = new AMQPExchange($channel);
+ $exchange->publish('Four Arg Message', 'queue_test', AMQP_NOPARAM,
['headers' => ['foo' => 'bar']]);
+
+ expectEnvelope($queue, 'Four Arg Message', static function (AMQPEnvelope
$envelope): void {
+ assertTrue($envelope->hasHeader('sw8'), 'missing sw8 header for
four-arg publish');
+ assertTrue($envelope->hasHeader('foo'), 'missing custom header for
four-arg publish');
+ assertSameValue('bar', $envelope->getHeader('foo'), 'custom header
should be preserved');
+ });
+}
+
+{
+ $exchange = new AMQPExchange($channel);
+ $exchange->publish('Default Exchange Message', 'queue_test', AMQP_NOPARAM);
+
+ expectEnvelope($queue, 'Default Exchange Message', static function
(AMQPEnvelope $envelope): void {
+ assertTrue($envelope->hasHeader('sw8'), 'missing sw8 header for
default exchange publish');
+ });
+}
+
+echo "ok";