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