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.TimeUnit;
027
028import javax.management.AttributeValueExp;
029import javax.management.MBeanServer;
030import javax.management.ObjectName;
031import javax.management.Query;
032import javax.management.QueryExp;
033import javax.management.StringValueExp;
034import javax.management.openmbean.CompositeData;
035import javax.management.openmbean.CompositeDataSupport;
036import javax.management.openmbean.CompositeType;
037import javax.management.openmbean.TabularData;
038import javax.management.openmbean.TabularDataSupport;
039
040import org.apache.camel.CamelContext;
041import org.apache.camel.ManagementStatisticsLevel;
042import org.apache.camel.Route;
043import org.apache.camel.RuntimeCamelException;
044import org.apache.camel.ServiceStatus;
045import org.apache.camel.TimerListener;
046import org.apache.camel.api.management.ManagedResource;
047import org.apache.camel.api.management.mbean.CamelOpenMBeanTypes;
048import org.apache.camel.api.management.mbean.ManagedProcessorMBean;
049import org.apache.camel.api.management.mbean.ManagedRouteMBean;
050import org.apache.camel.api.management.mbean.ManagedStepMBean;
051import org.apache.camel.api.management.mbean.RouteError;
052import org.apache.camel.model.Model;
053import org.apache.camel.model.ModelHelper;
054import org.apache.camel.model.RouteDefinition;
055import org.apache.camel.spi.InflightRepository;
056import org.apache.camel.spi.ManagementStrategy;
057import org.apache.camel.spi.RoutePolicy;
058import org.apache.camel.util.ObjectHelper;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061
062@ManagedResource(description = "Managed Route")
063public class ManagedRoute extends ManagedPerformanceCounter implements TimerListener, ManagedRouteMBean {
064
065    public static final String VALUE_UNKNOWN = "Unknown";
066
067    private static final Logger LOG = LoggerFactory.getLogger(ManagedRoute.class);
068
069    protected final Route route;
070    protected final String description;
071    protected final CamelContext context;
072    private final LoadTriplet load = new LoadTriplet();
073    private final String jmxDomain;
074
075    public ManagedRoute(CamelContext 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
089    public Route getRoute() {
090        return route;
091    }
092
093    public CamelContext getContext() {
094        return context;
095    }
096
097    @Override
098    public String getRouteId() {
099        String id = route.getId();
100        if (id == null) {
101            id = VALUE_UNKNOWN;
102        }
103        return id;
104    }
105
106    @Override
107    public String getRouteGroup() {
108        return route.getGroup();
109    }
110
111    @Override
112    public TabularData getRouteProperties() {
113        try {
114            final Map<String, Object> properties = route.getProperties();
115            final TabularData answer = new TabularDataSupport(CamelOpenMBeanTypes.camelRoutePropertiesTabularType());
116            final CompositeType ct = CamelOpenMBeanTypes.camelRoutePropertiesCompositeType();
117
118            // gather route properties
119            for (Map.Entry<String, Object> entry : properties.entrySet()) {
120                final String key = entry.getKey();
121                final String val = context.getTypeConverter().convertTo(String.class, entry.getValue());
122
123                CompositeData data = new CompositeDataSupport(
124                    ct,
125                    new String[]{"key", "value"},
126                    new Object[]{key, val}
127                );
128
129                answer.put(data);
130            }
131            return answer;
132        } catch (Exception e) {
133            throw RuntimeCamelException.wrapRuntimeCamelException(e);
134        }
135    }
136
137    @Override
138    public String getDescription() {
139        return description;
140    }
141
142    @Override
143    public String getEndpointUri() {
144        if (route.getEndpoint() != null) {
145            return route.getEndpoint().getEndpointUri();
146        }
147        return VALUE_UNKNOWN;
148    }
149
150    @Override
151    public String getState() {
152        // must use String type to be sure remote JMX can read the attribute without requiring Camel classes.
153        ServiceStatus status = context.getRouteController().getRouteStatus(route.getId());
154        // if no status exists then its stopped
155        if (status == null) {
156            status = ServiceStatus.Stopped;
157        }
158        return status.name();
159    }
160
161    @Override
162    public String getUptime() {
163        return route.getUptime();
164    }
165
166    @Override
167    public long getUptimeMillis() {
168        return route.getUptimeMillis();
169    }
170
171    public Integer getInflightExchanges() {
172        return (int) super.getExchangesInflight();
173    }
174
175    @Override
176    public String getCamelId() {
177        return context.getName();
178    }
179
180    @Override
181    public String getCamelManagementName() {
182        return context.getManagementName();
183    }
184
185    @Override
186    public Boolean getTracing() {
187        return route.getRouteContext().isTracing();
188    }
189
190    @Override
191    public void setTracing(Boolean tracing) {
192        route.getRouteContext().setTracing(tracing);
193    }
194
195    @Override
196    public Boolean getMessageHistory() {
197        return route.getRouteContext().isMessageHistory();
198    }
199
200    @Override
201    public Boolean getLogMask() {
202        return route.getRouteContext().isLogMask();
203    }
204
205    @Override
206    public String getRoutePolicyList() {
207        List<RoutePolicy> policyList = route.getRouteContext().getRoutePolicyList();
208
209        if (policyList == null || policyList.isEmpty()) {
210            // return an empty string to have it displayed nicely in JMX consoles
211            return "";
212        }
213
214        StringBuilder sb = new StringBuilder();
215        for (int i = 0; i < policyList.size(); i++) {
216            RoutePolicy policy = policyList.get(i);
217            sb.append(policy.getClass().getSimpleName());
218            sb.append("(").append(ObjectHelper.getIdentityHashCode(policy)).append(")");
219            if (i < policyList.size() - 1) {
220                sb.append(", ");
221            }
222        }
223        return sb.toString();
224    }
225
226    @Override
227    public String getLoad01() {
228        double load1 = load.getLoad1();
229        if (Double.isNaN(load1)) {
230            // empty string if load statistics is disabled
231            return "";
232        } else {
233            return String.format("%.2f", load1);
234        }
235    }
236
237    @Override
238    public String getLoad05() {
239        double load5 = load.getLoad5();
240        if (Double.isNaN(load5)) {
241            // empty string if load statistics is disabled
242            return "";
243        } else {
244            return String.format("%.2f", load5);
245        }
246    }
247
248    @Override
249    public String getLoad15() {
250        double load15 = load.getLoad15();
251        if (Double.isNaN(load15)) {
252            // empty string if load statistics is disabled
253            return "";
254        } else {
255            return String.format("%.2f", load15);
256        }
257    }
258
259    @Override
260    public void onTimer() {
261        load.update(getInflightExchanges());
262    }
263
264    @Override
265    public void start() throws Exception {
266        if (!context.getStatus().isStarted()) {
267            throw new IllegalArgumentException("CamelContext is not started");
268        }
269        context.getRouteController().startRoute(getRouteId());
270    }
271
272    @Override
273    public void stop() throws Exception {
274        if (!context.getStatus().isStarted()) {
275            throw new IllegalArgumentException("CamelContext is not started");
276        }
277        context.getRouteController().stopRoute(getRouteId());
278    }
279
280    @Override
281    public void stop(long timeout) throws Exception {
282        if (!context.getStatus().isStarted()) {
283            throw new IllegalArgumentException("CamelContext is not started");
284        }
285        context.getRouteController().stopRoute(getRouteId(), timeout, TimeUnit.SECONDS);
286    }
287
288    @Override
289    public boolean stop(Long timeout, Boolean abortAfterTimeout) throws Exception {
290        if (!context.getStatus().isStarted()) {
291            throw new IllegalArgumentException("CamelContext is not started");
292        }
293        return context.getRouteController().stopRoute(getRouteId(), timeout, TimeUnit.SECONDS, abortAfterTimeout);
294    }
295
296    public void shutdown() throws Exception {
297        if (!context.getStatus().isStarted()) {
298            throw new IllegalArgumentException("CamelContext is not started");
299        }
300        String routeId = getRouteId();
301        context.getRouteController().stopRoute(routeId);
302        context.removeRoute(routeId);
303    }
304
305    public void shutdown(long timeout) throws Exception {
306        if (!context.getStatus().isStarted()) {
307            throw new IllegalArgumentException("CamelContext is not started");
308        }
309        String routeId = getRouteId();
310        context.getRouteController().stopRoute(routeId, timeout, TimeUnit.SECONDS);
311        context.removeRoute(routeId);
312    }
313
314    @Override
315    public boolean remove() throws Exception {
316        if (!context.getStatus().isStarted()) {
317            throw new IllegalArgumentException("CamelContext is not started");
318        }
319        return context.removeRoute(getRouteId());
320    }
321
322    @Override
323    public void restart() throws Exception {
324        restart(1);
325    }
326
327    @Override
328    public void restart(long delay) throws Exception {
329        stop();
330        if (delay > 0) {
331            try {
332                LOG.debug("Sleeping {} seconds before starting route: {}", delay, getRouteId());
333                Thread.sleep(delay * 1000);
334            } catch (InterruptedException e) {
335                // ignore
336            }
337        }
338        start();
339    }
340
341    @Override
342    public String dumpRouteAsXml() throws Exception {
343        return dumpRouteAsXml(false, false);
344    }
345
346    @Override
347    public String dumpRouteAsXml(boolean resolvePlaceholders) throws Exception {
348        return dumpRouteAsXml(resolvePlaceholders, false);
349    }
350
351    @Override
352    public String dumpRouteAsXml(boolean resolvePlaceholders, boolean resolveDelegateEndpoints) throws Exception {
353        String id = route.getId();
354        RouteDefinition def = context.getExtension(Model.class).getRouteDefinition(id);
355        if (def != null) {
356            return ModelHelper.dumpModelAsXml(context, def, resolvePlaceholders, resolveDelegateEndpoints);
357        }
358
359        return null;
360    }
361
362    @Override
363    public void updateRouteFromXml(String xml) throws Exception {
364        // convert to model from xml
365        RouteDefinition def = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class);
366        if (def == null) {
367            return;
368        }
369
370        // if the xml does not contain the route-id then we fix this by adding the actual route id
371        // this may be needed if the route-id was auto-generated, as the intend is to update this route
372        // and not add a new route, adding a new route, use the MBean operation on ManagedCamelContext instead.
373        if (ObjectHelper.isEmpty(def.getId())) {
374            def.setId(getRouteId());
375        } else if (!def.getId().equals(getRouteId())) {
376            throw new IllegalArgumentException("Cannot update route from XML as routeIds does not match. routeId: "
377                    + getRouteId() + ", routeId from XML: " + def.getId());
378        }
379
380        LOG.debug("Updating route: {} from xml: {}", def.getId(), xml);
381
382        try {
383            // add will remove existing route first
384            context.getExtension(Model.class).addRouteDefinition(def);
385        } catch (Exception e) {
386            // log the error as warn as the management api may be invoked remotely over JMX which does not propagate such exception
387            String msg = "Error updating route: " + def.getId() + " from xml: " + xml + " due: " + e.getMessage();
388            LOG.warn(msg, e);
389            throw e;
390        }
391    }
392
393    @Override
394    public String dumpRouteStatsAsXml(boolean fullStats, boolean includeProcessors) throws Exception {
395        // in this logic we need to calculate the accumulated processing time for the processor in the route
396        // and hence why the logic is a bit more complicated to do this, as we need to calculate that from
397        // the bottom -> top of the route but this information is valuable for profiling routes
398        StringBuilder sb = new StringBuilder();
399
400        // need to calculate this value first, as we need that value for the route stat
401        Long processorAccumulatedTime = 0L;
402
403        // gather all the processors for this route, which requires JMX
404        if (includeProcessors) {
405            sb.append("  <processorStats>\n");
406            MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
407            if (server != null) {
408                // get all the processor mbeans and sort them accordingly to their index
409                String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
410                ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*");
411                Set<ObjectName> names = server.queryNames(query, null);
412                List<ManagedProcessorMBean> mps = new ArrayList<>();
413                for (ObjectName on : names) {
414                    ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedProcessorMBean.class);
415
416                    // the processor must belong to this route
417                    if (getRouteId().equals(processor.getRouteId())) {
418                        mps.add(processor);
419                    }
420                }
421                mps.sort(new OrderProcessorMBeans());
422
423                // walk the processors in reverse order, and calculate the accumulated total time
424                Map<String, Long> accumulatedTimes = new HashMap<>();
425                Collections.reverse(mps);
426                for (ManagedProcessorMBean processor : mps) {
427                    processorAccumulatedTime += processor.getTotalProcessingTime();
428                    accumulatedTimes.put(processor.getProcessorId(), processorAccumulatedTime);
429                }
430                // and reverse back again
431                Collections.reverse(mps);
432
433                // and now add the sorted list of processors to the xml output
434                for (ManagedProcessorMBean processor : mps) {
435                    sb.append("    <processorStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", processor.getProcessorId(), processor.getIndex(), processor.getState()));
436                    // do we have an accumulated time then append that
437                    Long accTime = accumulatedTimes.get(processor.getProcessorId());
438                    if (accTime != null) {
439                        sb.append(" accumulatedProcessingTime=\"").append(accTime).append("\"");
440                    }
441                    // use substring as we only want the attributes
442                    sb.append(" ").append(processor.dumpStatsAsXml(fullStats).substring(7)).append("\n");
443                }
444            }
445            sb.append("  </processorStats>\n");
446        }
447
448        // route self time is route total - processor accumulated total)
449        long routeSelfTime = getTotalProcessingTime() - processorAccumulatedTime;
450        if (routeSelfTime < 0) {
451            // ensure we don't calculate that as negative
452            routeSelfTime = 0;
453        }
454
455        StringBuilder answer = new StringBuilder();
456        answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState()));
457        // use substring as we only want the attributes
458        String stat = dumpStatsAsXml(fullStats);
459        answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\"");
460        answer.append(" selfProcessingTime=\"").append(routeSelfTime).append("\"");
461        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
462        if (oldest == null) {
463            answer.append(" oldestInflightExchangeId=\"\"");
464            answer.append(" oldestInflightDuration=\"\"");
465        } else {
466            answer.append(" oldestInflightExchangeId=\"").append(oldest.getExchange().getExchangeId()).append("\"");
467            answer.append(" oldestInflightDuration=\"").append(oldest.getDuration()).append("\"");
468        }
469        answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n");
470
471        if (includeProcessors) {
472            answer.append(sb);
473        }
474
475        answer.append("</routeStat>");
476        return answer.toString();
477    }
478
479    @Override
480    public String dumpStepStatsAsXml(boolean fullStats) throws Exception {
481        // in this logic we need to calculate the accumulated processing time for the processor in the route
482        // and hence why the logic is a bit more complicated to do this, as we need to calculate that from
483        // the bottom -> top of the route but this information is valuable for profiling routes
484        StringBuilder sb = new StringBuilder();
485
486        // gather all the steps for this route, which requires JMX
487        sb.append("  <stepStats>\n");
488        MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
489        if (server != null) {
490            // get all the processor mbeans and sort them accordingly to their index
491            String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
492            ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=steps,*");
493            Set<ObjectName> names = server.queryNames(query, null);
494            List<ManagedStepMBean> mps = new ArrayList<>();
495            for (ObjectName on : names) {
496                ManagedStepMBean step = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedStepMBean.class);
497
498                // the step must belong to this route
499                if (getRouteId().equals(step.getRouteId())) {
500                    mps.add(step);
501                }
502            }
503            mps.sort(new OrderProcessorMBeans());
504
505            // and now add the sorted list of steps to the xml output
506            for (ManagedStepMBean step : mps) {
507                sb.append("    <stepStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", step.getProcessorId(), step.getIndex(), step.getState()));
508                // use substring as we only want the attributes
509                sb.append(" ").append(step.dumpStatsAsXml(fullStats).substring(7)).append("\n");
510            }
511        }
512        sb.append("  </stepStats>\n");
513
514        StringBuilder answer = new StringBuilder();
515        answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState()));
516        // use substring as we only want the attributes
517        String stat = dumpStatsAsXml(fullStats);
518        answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\"");
519        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
520        if (oldest == null) {
521            answer.append(" oldestInflightExchangeId=\"\"");
522            answer.append(" oldestInflightDuration=\"\"");
523        } else {
524            answer.append(" oldestInflightExchangeId=\"").append(oldest.getExchange().getExchangeId()).append("\"");
525            answer.append(" oldestInflightDuration=\"").append(oldest.getDuration()).append("\"");
526        }
527        answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n");
528
529        answer.append(sb);
530
531        answer.append("</routeStat>");
532        return answer.toString();
533    }
534
535    @Override
536    public void reset(boolean includeProcessors) throws Exception {
537        reset();
538
539        // and now reset all processors for this route
540        if (includeProcessors) {
541            MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
542            if (server != null) {
543                // get all the processor mbeans and sort them accordingly to their index
544                String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
545                ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*");
546                QueryExp queryExp = Query.match(new AttributeValueExp("RouteId"), new StringValueExp(getRouteId()));
547                Set<ObjectName> names = server.queryNames(query, queryExp);
548                for (ObjectName name : names) {
549                    server.invoke(name, "reset", null, null);
550                }
551            }
552        }
553    }
554
555    @Override
556    public boolean equals(Object o) {
557        return this == o || (o != null && getClass() == o.getClass() && route.equals(((ManagedRoute) o).route));
558    }
559
560    @Override
561    public int hashCode() {
562        return route.hashCode();
563    }
564
565    private InflightRepository.InflightExchange getOldestInflightEntry() {
566        return getContext().getInflightRepository().oldest(getRouteId());
567    }
568
569    @Override
570    public Long getOldestInflightDuration() {
571        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
572        if (oldest == null) {
573            return null;
574        } else {
575            return oldest.getDuration();
576        }
577    }
578
579    @Override
580    public String getOldestInflightExchangeId() {
581        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
582        if (oldest == null) {
583            return null;
584        } else {
585            return oldest.getExchange().getExchangeId();
586        }
587    }
588
589    @Override
590    public Boolean getHasRouteController() {
591        return route.getRouteContext().getRouteController() != null;
592    }
593
594    @Override
595    public RouteError getLastError() {
596        org.apache.camel.spi.RouteError error = route.getRouteContext().getLastError();
597        if (error == null) {
598            return null;
599        } else {
600            return new RouteError() {
601                @Override
602                public Phase getPhase() {
603                    if (error.getPhase() != null) {
604                        switch (error.getPhase()) {
605                            case START: return Phase.START;
606                            case STOP: return Phase.STOP;
607                            case SUSPEND: return Phase.SUSPEND;
608                            case RESUME: return Phase.RESUME;
609                            case SHUTDOWN: return Phase.SHUTDOWN;
610                            case REMOVE: return Phase.REMOVE;
611                            default: throw new IllegalStateException();
612                        }
613                    }
614                    return null;
615                }
616
617                @Override
618                public Throwable getException() {
619                    return error.getException();
620                }
621            };
622        }
623    }
624
625    /**
626     * Used for sorting the processor mbeans accordingly to their index.
627     */
628    private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> {
629
630        @Override
631        public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) {
632            return o1.getIndex().compareTo(o2.getIndex());
633        }
634    }
635}