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