/**
* Copyright 2016-2017 Sixt GmbH & Co. Autovermietung KG
* Licensed 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.
*/
package com.sixt.service.framework.kafka.messaging;
import com.google.common.base.Strings;
import com.sixt.service.framework.OrangeContext;
import org.slf4j.Marker;
import static net.logstash.logback.marker.Markers.append;
/**
* Metadata information about the Message such as
* - If it was received from Kafka or is a newly created mesage.
* - Kafka related information such as topic, partition id, partitioning key and the offset of the message in the partition.
* - Header fields sent with the Message (in the Envolope), e.g. message id, type of the inner message, correlation ids, etc.
* <p>
* Depending on the message exchange pattern, some fields are optional.
* <p>
* For request-response, the request requires to have the reply-to topic set. The consumer of the request must send the response
* back to the reply-to topic. In the response, the requestCorrelationId is required and refers to the message id of the original request.
*/
public class Metadata {
// Immutable
// Keep in sync with Messaging.proto / message Envelope
// Inbound or outbound message, i.e. did we receive it or is it a newly created one?
private final boolean wasReceived;
// Kafka information
private final Topic topic; // The topic this message was received on (INBOUND) or is to be send to (OUTBOUND).
private final String partitioningKey; // The key used to determine the partition in the topic. May be null.
private final int partitionId; // The id of the topic partition - only for INBOUND.
private final long offset; // The offset of the message - only for INBOUND
// Message headers - see also Messaging.proto
private final String messageId;
// Tracing/correlation ids
private final String correlationId; // OPTIONAL. A correlation id to correlate multiple messages, rpc request/responses, etc. belonging to a trace/flow/user request/etc..
// Message exchange patterns
// -> request/reply
private final String requestCorrelationId; // REQUIRED for RESPONSE. This response correlates to the message id of the original request.
private final Topic replyTo; // REQUIRED for REQUEST. Send responses for this request to the given address. See class Topic for syntax.
private final MessageType type; // REQUIRED.
public boolean isInbound() {
return wasReceived();
}
public boolean isOutbound() {
return !isInbound();
}
public boolean wasReceived() {
return wasReceived;
}
public Topic getTopic() {
return topic;
}
public String getPartitioningKey() {
return partitioningKey;
}
public int getPartitionId() {
return partitionId;
}
public long getOffset() {
return offset;
}
public String getMessageId() {
return messageId;
}
public String getCorrelationId() {
return correlationId;
}
public String getRequestCorrelationId() {
return requestCorrelationId;
}
public Topic getReplyTo() {
return replyTo;
}
public MessageType getType() {
return type;
}
// Helper methods --------------------------
public OrangeContext newContextFromMetadata() {
return new OrangeContext(correlationId);
}
@Override
public String toString() {
return "Metadata{" +
"wasReceived=" + wasReceived +
", topic=" + topic +
", partitioningKey='" + partitioningKey + '\'' +
", partitionId=" + partitionId +
", offset=" + offset +
", messageId='" + messageId + '\'' +
", correlationId='" + correlationId + '\'' +
", requestCorrelationId='" + requestCorrelationId + '\'' +
", replyTo=" + replyTo +
", type=" + type +
'}';
}
public Marker getLoggingMarker() {
// If we get more optional header fields, we should probably exclude them if they are empty.
Marker messageMarker =
append("messageId", messageId)
.and(append("partitionId", partitionId))
.and(append("partitioningKey", partitioningKey))
.and(append("offset", offset))
.and(append("messageId", messageId))
.and(append("correlationId", correlationId))
.and(append("requestCorrelationId", requestCorrelationId));
// Nota bene: without the toString the marker tries to convert the object into Json, which produces strange results
if (topic != null) {
messageMarker.add(append("topic", topic.toString()));
}
if (replyTo != null) {
messageMarker.add(append("replyTo", replyTo.toString()));
}
if (type != null) {
messageMarker.add(append("messageType", type.toString()));
}
return messageMarker;
}
// Object instantiation is done via factory
Metadata(boolean wasReceived, Topic topic, String partitioningKey, int partitionId, long offset, String messageId, String correlationId, String requestCorrelationId, Topic replyTo, MessageType type) {
this.wasReceived = wasReceived;
if (topic == null || topic.isEmpty()) {
throw new IllegalArgumentException("topic is required");
}
this.topic = topic;
// null partitioningKey is ok, producer will select partition (round-robin)
this.partitioningKey = partitioningKey;
this.partitionId = partitionId;
this.offset = offset;
if (Strings.isNullOrEmpty(messageId)) {
throw new IllegalArgumentException("non-empty messageId is required");
}
this.messageId = messageId;
this.correlationId = correlationId;
this.requestCorrelationId = requestCorrelationId;
this.replyTo = replyTo;
if (type == null) {
throw new IllegalArgumentException("type is required");
}
this.type = type;
}
}