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