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