diff --git a/ci/publish-documentation-to-github-pages.sh b/ci/publish-documentation-to-github-pages.sh
index f1197cb5e5..074ae4efcf 100755
--- a/ci/publish-documentation-to-github-pages.sh
+++ b/ci/publish-documentation-to-github-pages.sh
@@ -2,8 +2,13 @@
. $(pwd)/release-versions.txt
+./mvnw clean test-compile exec:java \
+ -Dexec.mainClass=io.micrometer.docs.DocsGeneratorCommand \
+ -Dexec.classpathScope="test" \
+ -Dexec.args='src/main/java/com/rabbitmq/stream/observation/micrometer .* target/micrometer-observation-docs'
+
MESSAGE=$(git log -1 --pretty=%B)
-./mvnw clean buildnumber:create pre-site --no-transfer-progress
+./mvnw buildnumber:create pre-site --no-transfer-progress
./mvnw javadoc:javadoc -Dmaven.javadoc.skip=false --no-transfer-progress
diff --git a/pom.xml b/pom.xml
index 445ede3a78..913753754b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,6 +74,8 @@
2.10.10.10.41.2.5
+ 1.1.3
+ 1.0.23.11.03.1.22.7.6
@@ -281,7 +283,7 @@
org.eclipse.pahoorg.eclipse.paho.client.mqttv3${paho.version}
- test
+ test
@@ -312,6 +314,20 @@
test
+
+ io.micrometer
+ micrometer-tracing-integration-test
+ ${micrometer-tracing-test.version}
+ test
+
+
+
+ io.micrometer
+ micrometer-docs-generator
+ ${micrometer-docs-generator.version}
+ test
+
+
org.openjdk.jmhjmh-core
@@ -393,6 +409,9 @@
org.apache.maven.pluginsmaven-resources-plugin${maven-resources-plugin.version}
+
+ ${project.build.sourceEncoding}
+
@@ -457,6 +476,7 @@
-coderay../../test/java/com/rabbitmq/stream/docs
+ ${project.build.directory}
diff --git a/src/docs/asciidoc/appendixes.adoc b/src/docs/asciidoc/appendixes.adoc
new file mode 100644
index 0000000000..2cbda3e8c4
--- /dev/null
+++ b/src/docs/asciidoc/appendixes.adoc
@@ -0,0 +1,26 @@
+ifndef::build-directory[:build-directory: ../../../target]
+:test-examples: ../../test/java/com/rabbitmq/stream/docs
+
+[appendix]
+== Micrometer Observation
+
+It is possible to use https://micrometer.io/docs/observation[Micrometer Observation] to instrument publishing and consuming in the stream Java client.
+Micrometer Observation provides https://spring.io/blog/2022/10/12/observability-with-spring-boot-3[metrics, tracing, and log correlation with one single API].
+
+The stream Java client provides an `ObservationCollector` abstraction and an implementation for Micrometer Observation.
+The following snippet shows how to create and set up the Micrometer `ObservationCollector` implementation with an existing `ObservationRegistry`:
+
+.Configuring Micrometer Observation
+[source,java,indent=0]
+--------
+include::{test-examples}/EnvironmentUsage.java[tag=micrometer-observation]
+--------
+<1> Configure Micrometer `ObservationCollector` with builder
+<2> Set Micrometer `ObservationRegistry`
+
+The next sections document the conventions, spans, and metrics made available by the instrumentation.
+They are automatically generated from the source code with the https://github.com/micrometer-metrics/micrometer-docs-generator[Micrometer documentation generator].
+
+include::{build-directory}/micrometer-observation-docs/_conventions.adoc[]
+include::{build-directory}/micrometer-observation-docs/_spans.adoc[]
+include::{build-directory}/micrometer-observation-docs/_metrics.adoc[]
\ No newline at end of file
diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc
index 38bf91df28..0ff03dd549 100644
--- a/src/docs/asciidoc/index.adoc
+++ b/src/docs/asciidoc/index.adoc
@@ -1,6 +1,7 @@
= RabbitMQ Stream Java Client
:revnumber: {project-version}
:revremark: ({build-number})
+:appendix-caption: Appendix
ifndef::imagesdir[:imagesdir: images]
ifndef::sourcedir[:sourcedir: ../../main/java]
:source-highlighter: prettify
@@ -28,4 +29,6 @@ include::advanced-topics.adoc[]
include::building.adoc[]
-include::performance-tool.adoc[]
\ No newline at end of file
+include::performance-tool.adoc[]
+
+include::appendixes.adoc[]
\ No newline at end of file
diff --git a/src/main/java/com/rabbitmq/stream/Codec.java b/src/main/java/com/rabbitmq/stream/Codec.java
index 1eb5c88702..1eff41fc50 100644
--- a/src/main/java/com/rabbitmq/stream/Codec.java
+++ b/src/main/java/com/rabbitmq/stream/Codec.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
diff --git a/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java b/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java
index b6845ca73c..f4a730046a 100644
--- a/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java
+++ b/src/main/java/com/rabbitmq/stream/EnvironmentBuilder.java
@@ -222,6 +222,8 @@ public interface EnvironmentBuilder {
*/
EnvironmentBuilder metricsCollector(MetricsCollector metricsCollector);
+ EnvironmentBuilder observationCollector(ObservationCollector> observationCollector);
+
/**
* The maximum number of producers allocated to a single connection.
*
diff --git a/src/main/java/com/rabbitmq/stream/Message.java b/src/main/java/com/rabbitmq/stream/Message.java
index 6651a87c89..296007fc21 100644
--- a/src/main/java/com/rabbitmq/stream/Message.java
+++ b/src/main/java/com/rabbitmq/stream/Message.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2022 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
@@ -85,4 +85,31 @@ public interface Message {
* @return the message annotations
*/
Map getMessageAnnotations();
+
+ /**
+ * Add a message annotation to the message.
+ *
+ * @param key the message annotation key
+ * @param value the message annotation value
+ * @return the modified message
+ * @since 0.12.0
+ */
+ default Message annotate(String key, Object value) {
+ this.getMessageAnnotations().put(key, value);
+ return this;
+ }
+
+ /**
+ * Create a copy of the message.
+ *
+ *
The message copy contains the exact same instances of the original bare message (body,
+ * properties, application properties), only the message annotations are actually copied and can
+ * be modified independently.
+ *
+ * @return the message copy
+ * @since 0.12.0
+ */
+ default Message copy() {
+ return this;
+ }
}
diff --git a/src/main/java/com/rabbitmq/stream/ObservationCollector.java b/src/main/java/com/rabbitmq/stream/ObservationCollector.java
new file mode 100644
index 0000000000..e10c242de8
--- /dev/null
+++ b/src/main/java/com/rabbitmq/stream/ObservationCollector.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2023 VMware, Inc. or its affiliates. All rights reserved.
+//
+// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
+// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
+// For the MPL, please see LICENSE-MPL-RabbitMQ. For the ASL,
+// please see LICENSE-APACHE2.
+//
+// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
+// either express or implied. See the LICENSE file for specific language governing
+// rights and limitations of this software.
+//
+// If you have any questions regarding licensing, please contact us at
+// info@rabbitmq.com.
+package com.rabbitmq.stream;
+
+/**
+ * API to instrument operations in the stream client. The supported operations are publishing, and
+ * asynchronous delivery.
+ *
+ *
Implementations can gather information and send it to tracing backends. This allows e.g.
+ * following the processing steps of a given message through different systems.
+ *
+ *
This is considered an SPI and is susceptible to change at any time.
+ *
+ * @since 0.12.0
+ * @see EnvironmentBuilder#observationCollector(ObservationCollector)
+ * @see com.rabbitmq.stream.observation.micrometer.MicrometerObservationCollectorBuilder
+ */
+public interface ObservationCollector {
+
+ ObservationCollector NO_OP =
+ new ObservationCollector() {
+ @Override
+ public Void prePublish(String stream, Message message) {
+ return null;
+ }
+
+ @Override
+ public void published(Void context, Message message) {}
+
+ @Override
+ public MessageHandler subscribe(MessageHandler handler) {
+ return handler;
+ }
+ };
+
+ /**
+ * Start observation.
+ *
+ *
Implementations are expecting to return an observation context that will be passed in to the
+ * {@link #published(Object, Message)} callback.
+ *
+ * @param stream the stream to publish to
+ * @param message the message to publish
+ * @return observation context
+ */
+ T prePublish(String stream, Message message);
+
+ /**
+ * Callback when the message is about to be published.
+ *
+ * @param context the observation context
+ * @param message the message to publish
+ */
+ void published(T context, Message message);
+
+ /**
+ * Decorate consumer registration.
+ *
+ * @param handler the original handler
+ * @return a decorated handler
+ */
+ MessageHandler subscribe(MessageHandler handler);
+
+ /**
+ * Says whether the implementation does nothing or not.
+ *
+ * @return true if the implementation is a no-op
+ */
+ default boolean isNoop() {
+ return this == NO_OP;
+ }
+}
diff --git a/src/main/java/com/rabbitmq/stream/codec/QpidProtonCodec.java b/src/main/java/com/rabbitmq/stream/codec/QpidProtonCodec.java
index 094c619341..3a316cf279 100644
--- a/src/main/java/com/rabbitmq/stream/codec/QpidProtonCodec.java
+++ b/src/main/java/com/rabbitmq/stream/codec/QpidProtonCodec.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2022 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
@@ -32,6 +32,7 @@
import org.apache.qpid.proton.codec.WritableBuffer;
public class QpidProtonCodec implements Codec {
+
private static final Function MESSAGE_ANNOTATIONS_STRING_KEY_EXTRACTOR = k -> k;
private static final Function MESSAGE_ANNOTATIONS_SYMBOL_KEY_EXTRACTOR =
Symbol::toString;
@@ -52,7 +53,7 @@ private static Map createMessageAnnotations(
return createMapFromAmqpMap(
MESSAGE_ANNOTATIONS_SYMBOL_KEY_EXTRACTOR, message.getMessageAnnotations().getValue());
} else {
- return null;
+ return new LinkedHashMap<>();
}
}
@@ -258,7 +259,7 @@ public Message decode(byte[] data) {
createMessageAnnotations(message));
}
- protected Properties createProperties(org.apache.qpid.proton.message.Message message) {
+ protected static Properties createProperties(org.apache.qpid.proton.message.Message message) {
if (message.getProperties() != null) {
return new QpidProtonProperties(message.getProperties());
} else {
@@ -504,6 +505,21 @@ public Map getApplicationProperties() {
public Map getMessageAnnotations() {
return messageAnnotations;
}
+
+ @Override
+ public Message annotate(String key, Object value) {
+ this.messageAnnotations.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Message copy() {
+ return new QpidProtonMessage(
+ message,
+ createProperties(message),
+ createApplicationProperties(message),
+ createMessageAnnotations(message));
+ }
}
static class QpidProtonAmqpMessageWrapper implements Message {
@@ -579,6 +595,38 @@ public Map getMessageAnnotations() {
return null;
}
}
+
+ @Override
+ public Message annotate(String key, Object value) {
+ MessageAnnotations annotations = this.message.getMessageAnnotations();
+ if (annotations == null) {
+ annotations = new MessageAnnotations(new LinkedHashMap<>());
+ this.message.setMessageAnnotations(annotations);
+ }
+ annotations.getValue().put(Symbol.getSymbol(key), value);
+ return this;
+ }
+
+ @Override
+ public Message copy() {
+ org.apache.qpid.proton.message.Message copy =
+ org.apache.qpid.proton.message.Message.Factory.create();
+ copy.setProperties(this.message.getProperties());
+ copy.setBody(this.message.getBody());
+ copy.setApplicationProperties(this.message.getApplicationProperties());
+ if (this.message.getMessageAnnotations() != null) {
+ Map annotations = message.getMessageAnnotations().getValue();
+ Map annotationCopy;
+ if (annotations == null) {
+ annotationCopy = null;
+ } else {
+ annotationCopy = new LinkedHashMap<>(annotations.size());
+ annotationCopy.putAll(annotations);
+ }
+ copy.setMessageAnnotations(new MessageAnnotations(annotationCopy));
+ }
+ return new QpidProtonAmqpMessageWrapper(this.hasPublishingId, this.publishingId, copy);
+ }
}
// from
diff --git a/src/main/java/com/rabbitmq/stream/codec/QpidProtonMessageBuilder.java b/src/main/java/com/rabbitmq/stream/codec/QpidProtonMessageBuilder.java
index c94b2b727b..445060d608 100644
--- a/src/main/java/com/rabbitmq/stream/codec/QpidProtonMessageBuilder.java
+++ b/src/main/java/com/rabbitmq/stream/codec/QpidProtonMessageBuilder.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
diff --git a/src/main/java/com/rabbitmq/stream/codec/SimpleCodec.java b/src/main/java/com/rabbitmq/stream/codec/SimpleCodec.java
index fba102f34d..7c45a63497 100644
--- a/src/main/java/com/rabbitmq/stream/codec/SimpleCodec.java
+++ b/src/main/java/com/rabbitmq/stream/codec/SimpleCodec.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
diff --git a/src/main/java/com/rabbitmq/stream/codec/SwiftMqCodec.java b/src/main/java/com/rabbitmq/stream/codec/SwiftMqCodec.java
index 6b630514e8..748c2d7044 100644
--- a/src/main/java/com/rabbitmq/stream/codec/SwiftMqCodec.java
+++ b/src/main/java/com/rabbitmq/stream/codec/SwiftMqCodec.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2022 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
@@ -276,11 +276,11 @@ public EncodedMessage encode(Message message) {
outboundMessage.writeContent(output);
return new EncodedMessage(output.getCount(), output.getBuffer());
} catch (IOException e) {
- throw new StreamException("Error while writing AMQP 1.0 message to output stream");
+ throw new StreamException("Error while writing AMQP 1.0 message to output stream", e);
}
}
- protected AMQPType convertToSwiftMqType(Object value) {
+ protected static AMQPType convertToSwiftMqType(Object value) {
if (value instanceof Boolean) {
return ((Boolean) value).booleanValue() ? AMQPBoolean.TRUE : AMQPBoolean.FALSE;
} else if (value instanceof Byte) {
@@ -355,7 +355,7 @@ protected Message createMessage(byte[] data) {
try {
amqpMessage = new AMQPMessage(data);
} catch (Exception e) {
- throw new StreamException("Error while decoding AMQP 1.0 message");
+ throw new StreamException("Error while decoding AMQP 1.0 message", e);
}
Object body = extractBody(amqpMessage);
@@ -648,5 +648,47 @@ public Map getMessageAnnotations() {
return null;
}
}
+
+ @Override
+ public Message annotate(String key, Object value) {
+ MessageAnnotations annotations = this.message.getMessageAnnotations();
+ Map map;
+ try {
+ if (annotations == null) {
+ map = new LinkedHashMap<>();
+ annotations = new MessageAnnotations(map);
+ this.message.setMessageAnnotations(annotations);
+ } else {
+ map = annotations.getValue();
+ }
+ map.put(new AMQPSymbol(key), convertToSwiftMqType(value));
+ annotations.setValue(map);
+ } catch (IOException e) {
+ throw new StreamException("Error while annotating SwiftMQ message", e);
+ }
+ return this;
+ }
+
+ @Override
+ public Message copy() {
+ AMQPMessage copy = new AMQPMessage();
+ copy.setProperties(this.message.getProperties());
+ if (this.message.getData() != null) {
+ this.message.getData().forEach(copy::addData);
+ }
+ copy.setApplicationProperties(this.message.getApplicationProperties());
+ MessageAnnotations annotations = this.message.getMessageAnnotations();
+ if (annotations != null) {
+ Map annotationCopy = null;
+ try {
+ annotationCopy = new LinkedHashMap<>(annotations.getValue().size());
+ annotationCopy.putAll(annotations.getValue());
+ copy.setMessageAnnotations(new MessageAnnotations(annotationCopy));
+ } catch (IOException e) {
+ throw new StreamException("Error while copying SwiftMQ message annotations", e);
+ }
+ }
+ return new SwiftMqAmqpMessageWrapper(this.hasPublishingId, this.publishingId, copy);
+ }
}
}
diff --git a/src/main/java/com/rabbitmq/stream/impl/MessageAccumulator.java b/src/main/java/com/rabbitmq/stream/impl/MessageAccumulator.java
index e40f6ada11..7be47f45e6 100644
--- a/src/main/java/com/rabbitmq/stream/impl/MessageAccumulator.java
+++ b/src/main/java/com/rabbitmq/stream/impl/MessageAccumulator.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
@@ -30,12 +30,14 @@ interface AccumulatedEntity {
long time();
- long publishindId();
+ long publishingId();
String filterValue();
Object encodedEntity();
StreamProducer.ConfirmationCallback confirmationCallback();
+
+ Object observationContext();
}
}
diff --git a/src/main/java/com/rabbitmq/stream/impl/SimpleMessageAccumulator.java b/src/main/java/com/rabbitmq/stream/impl/SimpleMessageAccumulator.java
index 5f01ea1416..b8d68eb920 100644
--- a/src/main/java/com/rabbitmq/stream/impl/SimpleMessageAccumulator.java
+++ b/src/main/java/com/rabbitmq/stream/impl/SimpleMessageAccumulator.java
@@ -1,4 +1,4 @@
-// Copyright (c) 2020-2021 VMware, Inc. or its affiliates. All rights reserved.
+// Copyright (c) 2020-2023 VMware, Inc. or its affiliates. All rights reserved.
//
// This software, the RabbitMQ Stream Java client library, is dual-licensed under the
// Mozilla Public License 2.0 ("MPL"), and the Apache License version 2 ("ASL").
@@ -13,11 +13,7 @@
// info@rabbitmq.com.
package com.rabbitmq.stream.impl;
-import com.rabbitmq.stream.Codec;
-import com.rabbitmq.stream.ConfirmationHandler;
-import com.rabbitmq.stream.ConfirmationStatus;
-import com.rabbitmq.stream.Message;
-import com.rabbitmq.stream.StreamException;
+import com.rabbitmq.stream.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -31,18 +27,23 @@ class SimpleMessageAccumulator implements MessageAccumulator {
protected final BlockingQueue messages;
protected final Clock clock;
private final int capacity;
- private final Codec codec;
+ protected final Codec codec;
private final int maxFrameSize;
private final ToLongFunction publishSequenceFunction;
private final Function filterValueExtractor;
+ final String stream;
+ final ObservationCollector