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