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