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