001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.management.mbean;
018
019import java.io.ByteArrayInputStream;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.ConcurrentSkipListMap;
030import java.util.concurrent.TimeUnit;
031import java.util.concurrent.atomic.AtomicBoolean;
032import javax.management.AttributeValueExp;
033import javax.management.MBeanServer;
034import javax.management.ObjectName;
035import javax.management.Query;
036import javax.management.QueryExp;
037import javax.management.StringValueExp;
038
039import org.w3c.dom.Document;
040
041import org.apache.camel.CamelContext;
042import org.apache.camel.Exchange;
043import org.apache.camel.ManagementStatisticsLevel;
044import org.apache.camel.Route;
045import org.apache.camel.ServiceStatus;
046import org.apache.camel.TimerListener;
047import org.apache.camel.api.management.ManagedResource;
048import org.apache.camel.api.management.mbean.ManagedProcessorMBean;
049import org.apache.camel.api.management.mbean.ManagedRouteMBean;
050import org.apache.camel.model.ModelCamelContext;
051import org.apache.camel.model.ModelHelper;
052import org.apache.camel.model.RouteDefinition;
053import org.apache.camel.spi.ManagementStrategy;
054import org.apache.camel.spi.RoutePolicy;
055import org.apache.camel.util.ObjectHelper;
056import org.apache.camel.util.XmlLineNumberParser;
057import org.slf4j.Logger;
058import org.slf4j.LoggerFactory;
059
060@ManagedResource(description = "Managed Route")
061public class ManagedRoute extends ManagedPerformanceCounter implements TimerListener, ManagedRouteMBean {
062
063    public static final String VALUE_UNKNOWN = "Unknown";
064
065    private static final Logger LOG = LoggerFactory.getLogger(ManagedRoute.class);
066
067    protected final Route route;
068    protected final String description;
069    protected final ModelCamelContext context;
070    private final LoadTriplet load = new LoadTriplet();
071    private final ConcurrentSkipListMap<InFlightKey, Long> exchangesInFlightStartTimestamps = new ConcurrentSkipListMap<InFlightKey, Long>();
072    private final ConcurrentHashMap<String, InFlightKey> exchangesInFlightKeys = new ConcurrentHashMap<String, InFlightKey>();
073
074    public ManagedRoute(ModelCamelContext context, Route route) {
075        this.route = route;
076        this.context = context;
077        this.description = route.getDescription();
078    }
079
080    @Override
081    public void init(ManagementStrategy strategy) {
082        super.init(strategy);
083        boolean enabled = context.getManagementStrategy().getManagementAgent().getStatisticsLevel() != ManagementStatisticsLevel.Off;
084        setStatisticsEnabled(enabled);
085
086        exchangesInFlightKeys.clear();
087        exchangesInFlightStartTimestamps.clear();
088    }
089
090    public Route getRoute() {
091        return route;
092    }
093
094    public CamelContext getContext() {
095        return context;
096    }
097
098    public String getRouteId() {
099        String id = route.getId();
100        if (id == null) {
101            id = VALUE_UNKNOWN;
102        }
103        return id;
104    }
105
106    public String getDescription() {
107        return description;
108    }
109
110    @Override
111    public String getEndpointUri() {
112        if (route.getEndpoint() != null) {
113            return route.getEndpoint().getEndpointUri();
114        }
115        return VALUE_UNKNOWN;
116    }
117
118    public String getState() {
119        // must use String type to be sure remote JMX can read the attribute without requiring Camel classes.
120        ServiceStatus status = context.getRouteStatus(route.getId());
121        // if no status exists then its stopped
122        if (status == null) {
123            status = ServiceStatus.Stopped;
124        }
125        return status.name();
126    }
127
128    public String getUptime() {
129        return route.getUptime();
130    }
131
132    public Integer getInflightExchanges() {
133        return (int) super.getExchangesInflight();
134    }
135
136    public String getCamelId() {
137        return context.getName();
138    }
139
140    public String getCamelManagementName() {
141        return context.getManagementName();
142    }
143
144    public Boolean getTracing() {
145        return route.getRouteContext().isTracing();
146    }
147
148    public void setTracing(Boolean tracing) {
149        route.getRouteContext().setTracing(tracing);
150    }
151
152    public Boolean getMessageHistory() {
153        return route.getRouteContext().isMessageHistory();
154    }
155
156    public String getRoutePolicyList() {
157        List<RoutePolicy> policyList = route.getRouteContext().getRoutePolicyList();
158
159        if (policyList == null || policyList.isEmpty()) {
160            // return an empty string to have it displayed nicely in JMX consoles
161            return "";
162        }
163
164        StringBuilder sb = new StringBuilder();
165        for (int i = 0; i < policyList.size(); i++) {
166            RoutePolicy policy = policyList.get(i);
167            sb.append(policy.getClass().getSimpleName());
168            sb.append("(").append(ObjectHelper.getIdentityHashCode(policy)).append(")");
169            if (i < policyList.size() - 1) {
170                sb.append(", ");
171            }
172        }
173        return sb.toString();
174    }
175
176    public String getLoad01() {
177        double load1 = load.getLoad1();
178        if (Double.isNaN(load1)) {
179            // empty string if load statistics is disabled
180            return "";
181        } else {
182            return String.format("%.2f", load1);
183        }
184    }
185
186    public String getLoad05() {
187        double load5 = load.getLoad5();
188        if (Double.isNaN(load5)) {
189            // empty string if load statistics is disabled
190            return "";
191        } else {
192            return String.format("%.2f", load5);
193        }
194    }
195
196    public String getLoad15() {
197        double load15 = load.getLoad15();
198        if (Double.isNaN(load15)) {
199            // empty string if load statistics is disabled
200            return "";
201        } else {
202            return String.format("%.2f", load15);
203        }
204    }
205
206    @Override
207    public void onTimer() {
208        load.update(getInflightExchanges());
209    }
210
211    public void start() throws Exception {
212        if (!context.getStatus().isStarted()) {
213            throw new IllegalArgumentException("CamelContext is not started");
214        }
215        context.startRoute(getRouteId());
216    }
217
218    public void stop() throws Exception {
219        if (!context.getStatus().isStarted()) {
220            throw new IllegalArgumentException("CamelContext is not started");
221        }
222        context.stopRoute(getRouteId());
223    }
224
225    public void stop(long timeout) throws Exception {
226        if (!context.getStatus().isStarted()) {
227            throw new IllegalArgumentException("CamelContext is not started");
228        }
229        context.stopRoute(getRouteId(), timeout, TimeUnit.SECONDS);
230    }
231
232    public boolean stop(Long timeout, Boolean abortAfterTimeout) throws Exception {
233        if (!context.getStatus().isStarted()) {
234            throw new IllegalArgumentException("CamelContext is not started");
235        }
236        return context.stopRoute(getRouteId(), timeout, TimeUnit.SECONDS, abortAfterTimeout);
237    }
238
239    public void shutdown() throws Exception {
240        if (!context.getStatus().isStarted()) {
241            throw new IllegalArgumentException("CamelContext is not started");
242        }
243        String routeId = getRouteId();
244        context.stopRoute(routeId);
245        context.removeRoute(routeId);
246    }
247
248    public void shutdown(long timeout) throws Exception {
249        if (!context.getStatus().isStarted()) {
250            throw new IllegalArgumentException("CamelContext is not started");
251        }
252        String routeId = getRouteId();
253        context.stopRoute(routeId, timeout, TimeUnit.SECONDS);
254        context.removeRoute(routeId);
255    }
256
257    public boolean remove() throws Exception {
258        if (!context.getStatus().isStarted()) {
259            throw new IllegalArgumentException("CamelContext is not started");
260        }
261        return context.removeRoute(getRouteId());
262    }
263
264    public String dumpRouteAsXml() throws Exception {
265        return dumpRouteAsXml(false);
266    }
267
268    @Override
269    public String dumpRouteAsXml(boolean resolvePlaceholders) throws Exception {
270        String id = route.getId();
271        RouteDefinition def = context.getRouteDefinition(id);
272        if (def != null) {
273            String xml = ModelHelper.dumpModelAsXml(context, def);
274
275            // if resolving placeholders we parse the xml, and resolve the property placeholders during parsing
276            if (resolvePlaceholders) {
277                final AtomicBoolean changed = new AtomicBoolean();
278                InputStream is = new ByteArrayInputStream(xml.getBytes());
279                Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() {
280                    @Override
281                    public String transform(String text) {
282                        try {
283                            String after = getContext().resolvePropertyPlaceholders(text);
284                            if (!changed.get()) {
285                                changed.set(!text.equals(after));
286                            }
287                            return after;
288                        } catch (Exception e) {
289                            // ignore
290                            return text;
291                        }
292                    }
293                });
294                // okay there were some property placeholder replaced so re-create the model
295                if (changed.get()) {
296                    xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom);
297                    RouteDefinition copy = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class);
298                    xml = ModelHelper.dumpModelAsXml(context, copy);
299                }
300            }
301            return xml;
302        }
303        return null;
304    }
305
306    public void updateRouteFromXml(String xml) throws Exception {
307        // convert to model from xml
308        RouteDefinition def = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class);
309        if (def == null) {
310            return;
311        }
312
313        // if the xml does not contain the route-id then we fix this by adding the actual route id
314        // this may be needed if the route-id was auto-generated, as the intend is to update this route
315        // and not add a new route, adding a new route, use the MBean operation on ManagedCamelContext instead.
316        if (ObjectHelper.isEmpty(def.getId())) {
317            def.setId(getRouteId());
318        } else if (!def.getId().equals(getRouteId())) {
319            throw new IllegalArgumentException("Cannot update route from XML as routeIds does not match. routeId: "
320                    + getRouteId() + ", routeId from XML: " + def.getId());
321        }
322
323        LOG.debug("Updating route: {} from xml: {}", def.getId(), xml);
324
325        try {
326            // add will remove existing route first
327            context.addRouteDefinition(def);
328        } catch (Exception e) {
329            // log the error as warn as the management api may be invoked remotely over JMX which does not propagate such exception
330            String msg = "Error updating route: " + def.getId() + " from xml: " + xml + " due: " + e.getMessage();
331            LOG.warn(msg, e);
332            throw e;
333        }
334    }
335
336    public String dumpRouteStatsAsXml(boolean fullStats, boolean includeProcessors) throws Exception {
337        // in this logic we need to calculate the accumulated processing time for the processor in the route
338        // and hence why the logic is a bit more complicated to do this, as we need to calculate that from
339        // the bottom -> top of the route but this information is valuable for profiling routes
340        StringBuilder sb = new StringBuilder();
341
342        // need to calculate this value first, as we need that value for the route stat
343        Long processorAccumulatedTime = 0L;
344
345        // gather all the processors for this route, which requires JMX
346        if (includeProcessors) {
347            sb.append("  <processorStats>\n");
348            MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
349            if (server != null) {
350                // get all the processor mbeans and sort them accordingly to their index
351                String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
352                ObjectName query = ObjectName.getInstance("org.apache.camel:context=" + prefix + getContext().getManagementName() + ",type=processors,*");
353                Set<ObjectName> names = server.queryNames(query, null);
354                List<ManagedProcessorMBean> mps = new ArrayList<ManagedProcessorMBean>();
355                for (ObjectName on : names) {
356                    ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedProcessorMBean.class);
357
358                    // the processor must belong to this route
359                    if (getRouteId().equals(processor.getRouteId())) {
360                        mps.add(processor);
361                    }
362                }
363                Collections.sort(mps, new OrderProcessorMBeans());
364
365                // walk the processors in reverse order, and calculate the accumulated total time
366                Map<String, Long> accumulatedTimes = new HashMap<String, Long>();
367                Collections.reverse(mps);
368                for (ManagedProcessorMBean processor : mps) {
369                    processorAccumulatedTime += processor.getTotalProcessingTime();
370                    accumulatedTimes.put(processor.getProcessorId(), processorAccumulatedTime);
371                }
372                // and reverse back again
373                Collections.reverse(mps);
374
375                // and now add the sorted list of processors to the xml output
376                for (ManagedProcessorMBean processor : mps) {
377                    sb.append("    <processorStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", processor.getProcessorId(), processor.getIndex(), processor.getState()));
378                    // do we have an accumulated time then append that
379                    Long accTime = accumulatedTimes.get(processor.getProcessorId());
380                    if (accTime != null) {
381                        sb.append(" accumulatedProcessingTime=\"").append(accTime).append("\"");
382                    }
383                    // use substring as we only want the attributes
384                    sb.append(" ").append(processor.dumpStatsAsXml(fullStats).substring(7)).append("\n");
385                }
386            }
387            sb.append("  </processorStats>\n");
388        }
389
390        // route self time is route total - processor accumulated total)
391        long routeSelfTime = getTotalProcessingTime() - processorAccumulatedTime;
392        if (routeSelfTime < 0) {
393            // ensure we don't calculate that as negative
394            routeSelfTime = 0;
395        }
396
397        StringBuilder answer = new StringBuilder();
398        answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState()));
399        // use substring as we only want the attributes
400        String stat = dumpStatsAsXml(fullStats);
401        answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\"");
402        answer.append(" selfProcessingTime=\"").append(routeSelfTime).append("\"");
403        InFlightKey oldestInflightEntry = getOldestInflightEntry();
404        if (oldestInflightEntry == null) {
405            answer.append(" oldestInflightExchangeId=\"\"");
406            answer.append(" oldestInflightDuration=\"\"");
407        } else {
408            answer.append(" oldestInflightExchangeId=\"").append(oldestInflightEntry.exchangeId).append("\"");
409            answer.append(" oldestInflightDuration=\"").append(System.currentTimeMillis() - oldestInflightEntry.timeStamp).append("\"");
410        }
411        answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n");
412
413        if (includeProcessors) {
414            answer.append(sb);
415        }
416
417        answer.append("</routeStat>");
418        return answer.toString();
419    }
420
421    public void reset(boolean includeProcessors) throws Exception {
422        reset();
423
424        // and now reset all processors for this route
425        if (includeProcessors) {
426            MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
427            if (server != null) {
428                // get all the processor mbeans and sort them accordingly to their index
429                String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
430                ObjectName query = ObjectName.getInstance("org.apache.camel:context=" + prefix + getContext().getManagementName() + ",type=processors,*");
431                QueryExp queryExp = Query.match(new AttributeValueExp("RouteId"), new StringValueExp(getRouteId()));
432                Set<ObjectName> names = server.queryNames(query, queryExp);
433                for (ObjectName name : names) {
434                    server.invoke(name, "reset", null, null);
435                }
436            }
437        }
438    }
439
440    public String createRouteStaticEndpointJson() {
441        return getContext().createRouteStaticEndpointJson(getRouteId());
442    }
443
444    @Override
445    public String createRouteStaticEndpointJson(boolean includeDynamic) {
446        return getContext().createRouteStaticEndpointJson(getRouteId(), includeDynamic);
447    }
448
449    @Override
450    public boolean equals(Object o) {
451        return this == o || (o != null && getClass() == o.getClass() && route.equals(((ManagedRoute) o).route));
452    }
453
454    @Override
455    public int hashCode() {
456        return route.hashCode();
457    }
458
459    private InFlightKey getOldestInflightEntry() {
460        Map.Entry<InFlightKey, Long> entry = exchangesInFlightStartTimestamps.firstEntry();
461        if (entry != null) {
462            return entry.getKey();
463        }
464        return null;
465    }
466
467    public Long getOldestInflightDuration() {
468        InFlightKey oldest = getOldestInflightEntry();
469        if (oldest == null) {
470            return null;
471        }
472        return System.currentTimeMillis() - oldest.timeStamp;
473    }
474
475    public String getOldestInflightExchangeId() {
476        InFlightKey oldest = getOldestInflightEntry();
477        if (oldest == null) {
478            return null;
479        }
480        return oldest.exchangeId;
481    }
482
483    @Override
484    public synchronized void processExchange(Exchange exchange) {
485        InFlightKey key = new InFlightKey(System.currentTimeMillis(), exchange.getExchangeId());
486        InFlightKey oldKey = exchangesInFlightKeys.putIfAbsent(exchange.getExchangeId(), key);
487        // we may already have the exchange being processed so only add to timestamp if its a new exchange
488        // for example when people call the same routes recursive
489        if (oldKey == null) {
490            exchangesInFlightStartTimestamps.put(key, key.timeStamp);
491        }
492        super.processExchange(exchange);
493    }
494
495    @Override
496    public synchronized void completedExchange(Exchange exchange, long time) {
497        InFlightKey key = exchangesInFlightKeys.remove(exchange.getExchangeId());
498        if (key != null) {
499            exchangesInFlightStartTimestamps.remove(key);
500        }
501        super.completedExchange(exchange, time);
502    }
503
504    @Override
505    public synchronized void failedExchange(Exchange exchange) {
506        InFlightKey key = exchangesInFlightKeys.remove(exchange.getExchangeId());
507        if (key != null) {
508            exchangesInFlightStartTimestamps.remove(key);
509        }
510        super.failedExchange(exchange);
511    }
512
513    private static class InFlightKey implements Comparable<InFlightKey> {
514
515        private final Long timeStamp;
516        private final String exchangeId;
517
518        InFlightKey(Long timeStamp, String exchangeId) {
519            this.timeStamp = timeStamp;
520            this.exchangeId = exchangeId;
521        }
522
523        @Override
524        public int compareTo(InFlightKey o) {
525            int compare = Long.compare(timeStamp, o.timeStamp);
526            if (compare == 0) {
527                return exchangeId.compareTo(o.exchangeId);
528            }
529            return compare;
530        }
531
532        @Override
533        public boolean equals(Object o) {
534            if (this == o) {
535                return true;
536            }
537            if (o == null || getClass() != o.getClass()) {
538                return false;
539            }
540
541            InFlightKey that = (InFlightKey) o;
542
543            if (!exchangeId.equals(that.exchangeId)) {
544                return false;
545            }
546            if (!timeStamp.equals(that.timeStamp)) {
547                return false;
548            }
549
550            return true;
551        }
552
553        @Override
554        public int hashCode() {
555            int result = timeStamp.hashCode();
556            result = 31 * result + exchangeId.hashCode();
557            return result;
558        }
559
560        @Override
561        public String toString() {
562            return exchangeId;
563        }
564    }
565
566    /**
567     * Used for sorting the processor mbeans accordingly to their index.
568     */
569    private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> {
570
571        @Override
572        public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) {
573            return o1.getIndex().compareTo(o2.getIndex());
574        }
575    }
576}