/**
* 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.
*/
package org.apache.camel.processor.interceptor;
import java.util.Date;
import java.util.EventObject;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.LoggingLevel;
import org.apache.camel.NoTypeConversionAvailableException;
import org.apache.camel.Predicate;
import org.apache.camel.Processor;
import org.apache.camel.api.management.mbean.BacklogTracerEventMessage;
import org.apache.camel.impl.BreakpointSupport;
import org.apache.camel.impl.DefaultDebugger;
import org.apache.camel.management.event.ExchangeCompletedEvent;
import org.apache.camel.model.ProcessorDefinition;
import org.apache.camel.model.ProcessorDefinitionHelper;
import org.apache.camel.spi.Condition;
import org.apache.camel.spi.Debugger;
import org.apache.camel.spi.InterceptStrategy;
import org.apache.camel.support.ServiceSupport;
import org.apache.camel.util.CamelLogger;
import org.apache.camel.util.MessageHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link org.apache.camel.spi.Debugger} that has easy debugging functionality which
* can be used from JMX with {@link org.apache.camel.api.management.mbean.ManagedBacklogDebuggerMBean}.
* <p/>
* This implementation allows to set breakpoints (with or without a condition) and inspect the {@link Exchange}
* dumped in XML in {@link BacklogTracerEventMessage} format. There is operations to resume suspended breakpoints
* to continue routing the {@link Exchange}. There is also step functionality so you can single step a given
* {@link Exchange}.
* <p/>
* This implementation will only break the first {@link Exchange} that arrives to a breakpoint. If Camel routes using
* concurrency then sub-sequent {@link Exchange} will continue to be routed, if there breakpoint already holds a
* suspended {@link Exchange}.
*/
public class BacklogDebugger extends ServiceSupport implements InterceptStrategy {
private static final Logger LOG = LoggerFactory.getLogger(BacklogDebugger.class);
private long fallbackTimeout = 300;
private final CamelContext camelContext;
private LoggingLevel loggingLevel = LoggingLevel.INFO;
private final CamelLogger logger = new CamelLogger(LOG, loggingLevel);
private final AtomicBoolean enabled = new AtomicBoolean();
private final AtomicLong debugCounter = new AtomicLong(0);
private final Debugger debugger;
private final ConcurrentMap<String, NodeBreakpoint> breakpoints = new ConcurrentHashMap<String, NodeBreakpoint>();
private final ConcurrentMap<String, SuspendedExchange> suspendedBreakpoints = new ConcurrentHashMap<String, SuspendedExchange>();
private final ConcurrentMap<String, BacklogTracerEventMessage> suspendedBreakpointMessages = new ConcurrentHashMap<String, BacklogTracerEventMessage>();
private volatile String singleStepExchangeId;
private int bodyMaxChars = 128 * 1024;
private boolean bodyIncludeStreams;
private boolean bodyIncludeFiles = true;
/**
* A suspend {@link Exchange} at a breakpoint.
*/
private static final class SuspendedExchange {
private final Exchange exchange;
private final CountDownLatch latch;
/**
* @param exchange the suspend exchange
* @param latch the latch to use to continue routing the exchange
*/
private SuspendedExchange(Exchange exchange, CountDownLatch latch) {
this.exchange = exchange;
this.latch = latch;
}
public Exchange getExchange() {
return exchange;
}
public CountDownLatch getLatch() {
return latch;
}
}
public BacklogDebugger(CamelContext camelContext) {
this.camelContext = camelContext;
DefaultDebugger debugger = new DefaultDebugger(camelContext);
debugger.setUseTracer(false);
this.debugger = debugger;
}
@Override
@Deprecated
public Processor wrapProcessorInInterceptors(CamelContext context, ProcessorDefinition<?> definition, Processor target, Processor nextTarget) throws Exception {
throw new UnsupportedOperationException("Deprecated");
}
/**
* A helper method to return the BacklogDebugger instance if one is enabled
*
* @return the backlog debugger or null if none can be found
*/
public static BacklogDebugger getBacklogDebugger(CamelContext context) {
List<InterceptStrategy> list = context.getInterceptStrategies();
for (InterceptStrategy interceptStrategy : list) {
if (interceptStrategy instanceof BacklogDebugger) {
return (BacklogDebugger) interceptStrategy;
}
}
return null;
}
public Debugger getDebugger() {
return debugger;
}
public String getLoggingLevel() {
return loggingLevel.name();
}
public void setLoggingLevel(String level) {
loggingLevel = LoggingLevel.valueOf(level);
logger.setLevel(loggingLevel);
}
public void enableDebugger() {
logger.log("Enabling debugger");
try {
ServiceHelper.startService(debugger);
enabled.set(true);
} catch (Exception e) {
throw ObjectHelper.wrapRuntimeCamelException(e);
}
}
public void disableDebugger() {
logger.log("Disabling debugger");
try {
enabled.set(false);
ServiceHelper.stopService(debugger);
} catch (Exception e) {
// ignore
}
clearBreakpoints();
}
public boolean isEnabled() {
return enabled.get();
}
public boolean hasBreakpoint(String nodeId) {
return breakpoints.containsKey(nodeId);
}
public boolean isSingleStepMode() {
return singleStepExchangeId != null;
}
public void addBreakpoint(String nodeId) {
NodeBreakpoint breakpoint = breakpoints.get(nodeId);
if (breakpoint == null) {
logger.log("Adding breakpoint " + nodeId);
breakpoint = new NodeBreakpoint(nodeId, null);
breakpoints.put(nodeId, breakpoint);
debugger.addBreakpoint(breakpoint, breakpoint);
} else {
breakpoint.setCondition(null);
}
}
public void addConditionalBreakpoint(String nodeId, String language, String predicate) {
Predicate condition = camelContext.resolveLanguage(language).createPredicate(predicate);
NodeBreakpoint breakpoint = breakpoints.get(nodeId);
if (breakpoint == null) {
logger.log("Adding conditional breakpoint " + nodeId + " [" + predicate + "]");
breakpoint = new NodeBreakpoint(nodeId, condition);
breakpoints.put(nodeId, breakpoint);
debugger.addBreakpoint(breakpoint, breakpoint);
} else if (breakpoint.getCondition() == null) {
logger.log("Updating to conditional breakpoint " + nodeId + " [" + predicate + "]");
debugger.removeBreakpoint(breakpoint);
breakpoints.put(nodeId, breakpoint);
debugger.addBreakpoint(breakpoint, breakpoint);
} else if (breakpoint.getCondition() != null) {
logger.log("Updating conditional breakpoint " + nodeId + " [" + predicate + "]");
breakpoint.setCondition(condition);
}
}
public void removeBreakpoint(String nodeId) {
logger.log("Removing breakpoint " + nodeId);
// when removing a break point then ensure latches is cleared and counted down so we wont have hanging threads
suspendedBreakpointMessages.remove(nodeId);
SuspendedExchange se = suspendedBreakpoints.remove(nodeId);
NodeBreakpoint breakpoint = breakpoints.remove(nodeId);
if (breakpoint != null) {
debugger.removeBreakpoint(breakpoint);
}
if (se != null) {
se.getLatch().countDown();
}
}
public void removeAllBreakpoints() {
// stop single stepping
singleStepExchangeId = null;
for (String nodeId : getSuspendedBreakpointNodeIds()) {
removeBreakpoint(nodeId);
}
}
public Set<String> getBreakpoints() {
return new LinkedHashSet<String>(breakpoints.keySet());
}
public void resumeBreakpoint(String nodeId) {
resumeBreakpoint(nodeId, false);
}
private void resumeBreakpoint(String nodeId, boolean stepMode) {
logger.log("Resume breakpoint " + nodeId);
if (!stepMode) {
if (singleStepExchangeId != null) {
debugger.stopSingleStepExchange(singleStepExchangeId);
singleStepExchangeId = null;
}
}
// remember to remove the dumped message as its no longer in need
suspendedBreakpointMessages.remove(nodeId);
SuspendedExchange se = suspendedBreakpoints.remove(nodeId);
if (se != null) {
se.getLatch().countDown();
}
}
public void setMessageBodyOnBreakpoint(String nodeId, Object body) {
SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
boolean remove = body == null;
if (remove) {
removeMessageBodyOnBreakpoint(nodeId);
} else {
Class<?> oldType;
if (se.getExchange().hasOut()) {
oldType = se.getExchange().getOut().getBody() != null ? se.getExchange().getOut().getBody().getClass() : null;
} else {
oldType = se.getExchange().getIn().getBody() != null ? se.getExchange().getIn().getBody().getClass() : null;
}
setMessageBodyOnBreakpoint(nodeId, body, oldType);
}
}
}
public void setMessageBodyOnBreakpoint(String nodeId, Object body, Class<?> type) {
SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
boolean remove = body == null;
if (remove) {
removeMessageBodyOnBreakpoint(nodeId);
} else {
logger.log("Breakpoint at node " + nodeId + " is updating message body on exchangeId: " + se.getExchange().getExchangeId() + " with new body: " + body);
if (se.getExchange().hasOut()) {
// preserve type
if (type != null) {
se.getExchange().getOut().setBody(body, type);
} else {
se.getExchange().getOut().setBody(body);
}
} else {
if (type != null) {
se.getExchange().getIn().setBody(body, type);
} else {
se.getExchange().getIn().setBody(body);
}
}
}
}
}
public void removeMessageBodyOnBreakpoint(String nodeId) {
SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
logger.log("Breakpoint at node " + nodeId + " is removing message body on exchangeId: " + se.getExchange().getExchangeId());
if (se.getExchange().hasOut()) {
se.getExchange().getOut().setBody(null);
} else {
se.getExchange().getIn().setBody(null);
}
}
}
public void setMessageHeaderOnBreakpoint(String nodeId, String headerName, Object value) throws NoTypeConversionAvailableException {
SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
Class<?> oldType;
if (se.getExchange().hasOut()) {
oldType = se.getExchange().getOut().getHeader(headerName) != null ? se.getExchange().getOut().getHeader(headerName).getClass() : null;
} else {
oldType = se.getExchange().getIn().getHeader(headerName) != null ? se.getExchange().getIn().getHeader(headerName).getClass() : null;
}
setMessageHeaderOnBreakpoint(nodeId, headerName, value, oldType);
}
}
public void setMessageHeaderOnBreakpoint(String nodeId, String headerName, Object value, Class<?> type) throws NoTypeConversionAvailableException {
SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
logger.log("Breakpoint at node " + nodeId + " is updating message header on exchangeId: " + se.getExchange().getExchangeId() + " with header: " + headerName + " and value: " + value);
if (se.getExchange().hasOut()) {
if (type != null) {
Object convertedValue = se.getExchange().getContext().getTypeConverter().mandatoryConvertTo(type, se.getExchange(), value);
se.getExchange().getOut().setHeader(headerName, convertedValue);
} else {
se.getExchange().getOut().setHeader(headerName, value);
}
} else {
if (type != null) {
Object convertedValue = se.getExchange().getContext().getTypeConverter().mandatoryConvertTo(type, se.getExchange(), value);
se.getExchange().getIn().setHeader(headerName, convertedValue);
} else {
se.getExchange().getIn().setHeader(headerName, value);
}
}
}
}
public long getFallbackTimeout() {
return fallbackTimeout;
}
public void setFallbackTimeout(long fallbackTimeout) {
this.fallbackTimeout = fallbackTimeout;
}
public void removeMessageHeaderOnBreakpoint(String nodeId, String headerName) {
SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
logger.log("Breakpoint at node " + nodeId + " is removing message header on exchangeId: " + se.getExchange().getExchangeId() + " with header: " + headerName);
if (se.getExchange().hasOut()) {
se.getExchange().getOut().removeHeader(headerName);
} else {
se.getExchange().getIn().removeHeader(headerName);
}
}
}
public void resumeAll() {
logger.log("Resume all");
// stop single stepping
singleStepExchangeId = null;
for (String node : getSuspendedBreakpointNodeIds()) {
// remember to remove the dumped message as its no longer in need
suspendedBreakpointMessages.remove(node);
SuspendedExchange se = suspendedBreakpoints.remove(node);
if (se != null) {
se.getLatch().countDown();
}
}
}
public void stepBreakpoint(String nodeId) {
// if we are already in single step mode, then infer stepping
if (isSingleStepMode()) {
logger.log("stepBreakpoint " + nodeId + " is already in single step mode, so stepping instead.");
step();
}
logger.log("Step breakpoint " + nodeId);
// we want to step current exchange to next
BacklogTracerEventMessage msg = suspendedBreakpointMessages.get(nodeId);
NodeBreakpoint breakpoint = breakpoints.get(nodeId);
if (msg != null && breakpoint != null) {
singleStepExchangeId = msg.getExchangeId();
if (debugger.startSingleStepExchange(singleStepExchangeId, new StepBreakpoint())) {
// now resume
resumeBreakpoint(nodeId, true);
}
}
}
public void step() {
for (String node : getSuspendedBreakpointNodeIds()) {
// remember to remove the dumped message as its no longer in need
suspendedBreakpointMessages.remove(node);
SuspendedExchange se = suspendedBreakpoints.remove(node);
if (se != null) {
se.getLatch().countDown();
}
}
}
public Set<String> getSuspendedBreakpointNodeIds() {
return new LinkedHashSet<String>(suspendedBreakpoints.keySet());
}
/**
* Gets the exchanged suspended at the given breakpoint id or null if there is none at that id.
*
* @param id - node id for the breakpoint
* @return The suspended exchange or null if there isn't one suspended at the given breakpoint.
*/
public Exchange getSuspendedExchange(String id) {
SuspendedExchange suspendedExchange = suspendedBreakpoints.get(id);
return suspendedExchange != null ? suspendedExchange.getExchange() : null;
}
public void disableBreakpoint(String nodeId) {
logger.log("Disable breakpoint " + nodeId);
NodeBreakpoint breakpoint = breakpoints.get(nodeId);
if (breakpoint != null) {
breakpoint.suspend();
}
}
public void enableBreakpoint(String nodeId) {
logger.log("Enable breakpoint " + nodeId);
NodeBreakpoint breakpoint = breakpoints.get(nodeId);
if (breakpoint != null) {
breakpoint.activate();
}
}
public int getBodyMaxChars() {
return bodyMaxChars;
}
public void setBodyMaxChars(int bodyMaxChars) {
this.bodyMaxChars = bodyMaxChars;
}
public boolean isBodyIncludeStreams() {
return bodyIncludeStreams;
}
public void setBodyIncludeStreams(boolean bodyIncludeStreams) {
this.bodyIncludeStreams = bodyIncludeStreams;
}
public boolean isBodyIncludeFiles() {
return bodyIncludeFiles;
}
public void setBodyIncludeFiles(boolean bodyIncludeFiles) {
this.bodyIncludeFiles = bodyIncludeFiles;
}
public String dumpTracedMessagesAsXml(String nodeId) {
logger.log("Dump trace message from breakpoint " + nodeId);
BacklogTracerEventMessage msg = suspendedBreakpointMessages.get(nodeId);
if (msg != null) {
return msg.toXml(0);
} else {
return null;
}
}
public long getDebugCounter() {
return debugCounter.get();
}
public void resetDebugCounter() {
logger.log("Reset debug counter");
debugCounter.set(0);
}
public boolean beforeProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
return debugger.beforeProcess(exchange, processor, definition);
}
public boolean afterProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition, long timeTaken) {
// noop
return false;
}
protected void doStart() throws Exception {
// noop
}
protected void doStop() throws Exception {
if (enabled.get()) {
disableDebugger();
}
clearBreakpoints();
}
private void clearBreakpoints() {
// make sure to clear state and latches is counted down so we wont have hanging threads
breakpoints.clear();
for (SuspendedExchange se : suspendedBreakpoints.values()) {
se.getLatch().countDown();
}
suspendedBreakpoints.clear();
suspendedBreakpointMessages.clear();
}
/**
* Represents a {@link org.apache.camel.spi.Breakpoint} that has a {@link Condition} on a specific node id.
*/
private final class NodeBreakpoint extends BreakpointSupport implements Condition {
private final String nodeId;
private Predicate condition;
private NodeBreakpoint(String nodeId, Predicate condition) {
this.nodeId = nodeId;
this.condition = condition;
}
public Predicate getCondition() {
return condition;
}
public void setCondition(Predicate predicate) {
this.condition = predicate;
}
@Override
public void beforeProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
// store a copy of the message so we can see that from the debugger
Date timestamp = new Date();
String toNode = definition.getId();
String routeId = ProcessorDefinitionHelper.getRouteId(definition);
String exchangeId = exchange.getExchangeId();
String messageAsXml = MessageHelper.dumpAsXml(exchange.getIn(), true, 2, isBodyIncludeStreams(), isBodyIncludeFiles(), getBodyMaxChars());
long uid = debugCounter.incrementAndGet();
BacklogTracerEventMessage msg = new DefaultBacklogTracerEventMessage(uid, timestamp, routeId, toNode, exchangeId, messageAsXml);
suspendedBreakpointMessages.put(nodeId, msg);
// suspend at this breakpoint
final SuspendedExchange se = suspendedBreakpoints.get(nodeId);
if (se != null) {
// now wait until we should continue
logger.log("NodeBreakpoint at node " + toNode + " is waiting to continue for exchangeId: " + exchangeId);
try {
boolean hit = se.getLatch().await(fallbackTimeout, TimeUnit.SECONDS);
if (!hit) {
logger.log("NodeBreakpoint at node " + toNode + " timed out and is continued exchangeId: " + exchangeId, LoggingLevel.WARN);
} else {
logger.log("NodeBreakpoint at node " + toNode + " is continued exchangeId: " + exchangeId);
}
} catch (InterruptedException e) {
// ignore
}
}
}
@Override
public boolean matchProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
// must match node
if (!nodeId.equals(definition.getId())) {
return false;
}
// if condition then must match
if (condition != null && !condition.matches(exchange)) {
return false;
}
// we only want to break one exchange at a time, so if there is already a suspended breakpoint then do not match
SuspendedExchange se = new SuspendedExchange(exchange, new CountDownLatch(1));
boolean existing = suspendedBreakpoints.putIfAbsent(nodeId, se) != null;
return !existing;
}
@Override
public boolean matchEvent(Exchange exchange, EventObject event) {
return false;
}
}
/**
* Represents a {@link org.apache.camel.spi.Breakpoint} that is used during single step mode.
*/
private final class StepBreakpoint extends BreakpointSupport implements Condition {
@Override
public void beforeProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
// store a copy of the message so we can see that from the debugger
Date timestamp = new Date();
String toNode = definition.getId();
String routeId = ProcessorDefinitionHelper.getRouteId(definition);
String exchangeId = exchange.getExchangeId();
String messageAsXml = MessageHelper.dumpAsXml(exchange.getIn(), true, 2, isBodyIncludeStreams(), isBodyIncludeFiles(), getBodyMaxChars());
long uid = debugCounter.incrementAndGet();
BacklogTracerEventMessage msg = new DefaultBacklogTracerEventMessage(uid, timestamp, routeId, toNode, exchangeId, messageAsXml);
suspendedBreakpointMessages.put(toNode, msg);
// suspend at this breakpoint
SuspendedExchange se = new SuspendedExchange(exchange, new CountDownLatch(1));
suspendedBreakpoints.put(toNode, se);
// now wait until we should continue
logger.log("StepBreakpoint at node " + toNode + " is waiting to continue for exchangeId: " + exchange.getExchangeId());
try {
boolean hit = se.getLatch().await(fallbackTimeout, TimeUnit.SECONDS);
if (!hit) {
logger.log("StepBreakpoint at node " + toNode + " timed out and is continued exchangeId: " + exchange.getExchangeId(), LoggingLevel.WARN);
} else {
logger.log("StepBreakpoint at node " + toNode + " is continued exchangeId: " + exchange.getExchangeId());
}
} catch (InterruptedException e) {
// ignore
}
}
@Override
public boolean matchProcess(Exchange exchange, Processor processor, ProcessorDefinition<?> definition) {
return true;
}
@Override
public boolean matchEvent(Exchange exchange, EventObject event) {
return event instanceof ExchangeCompletedEvent;
}
@Override
public void onEvent(Exchange exchange, EventObject event, ProcessorDefinition<?> definition) {
// when the exchange is complete, we need to turn off single step mode if we were debug stepping the exchange
if (event instanceof ExchangeCompletedEvent) {
String completedId = ((ExchangeCompletedEvent) event).getExchange().getExchangeId();
if (singleStepExchangeId != null && singleStepExchangeId.equals(completedId)) {
logger.log("ExchangeId: " + completedId + " is completed, so exiting single step mode.");
singleStepExchangeId = null;
}
}
}
}
}