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