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    public String dumpRouteAsXml() throws Exception {
269        return dumpRouteAsXml(false);
270    }
271
272    @Override
273    public String dumpRouteAsXml(boolean resolvePlaceholders) throws Exception {
274        String id = route.getId();
275        RouteDefinition def = context.getRouteDefinition(id);
276        if (def != null) {
277            String xml = ModelHelper.dumpModelAsXml(context, def);
278
279            // if resolving placeholders we parse the xml, and resolve the property placeholders during parsing
280            if (resolvePlaceholders) {
281                final AtomicBoolean changed = new AtomicBoolean();
282                InputStream is = new ByteArrayInputStream(xml.getBytes("UTF-8"));
283                Document dom = XmlLineNumberParser.parseXml(is, new XmlLineNumberParser.XmlTextTransformer() {
284                    @Override
285                    public String transform(String text) {
286                        try {
287                            String after = getContext().resolvePropertyPlaceholders(text);
288                            if (!changed.get()) {
289                                changed.set(!text.equals(after));
290                            }
291                            return after;
292                        } catch (Exception e) {
293                            // ignore
294                            return text;
295                        }
296                    }
297                });
298                // okay there were some property placeholder replaced so re-create the model
299                if (changed.get()) {
300                    xml = context.getTypeConverter().mandatoryConvertTo(String.class, dom);
301                    RouteDefinition copy = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class);
302                    xml = ModelHelper.dumpModelAsXml(context, copy);
303                }
304            }
305            return xml;
306        }
307        return null;
308    }
309
310    public void updateRouteFromXml(String xml) throws Exception {
311        // convert to model from xml
312        RouteDefinition def = ModelHelper.createModelFromXml(context, xml, RouteDefinition.class);
313        if (def == null) {
314            return;
315        }
316
317        // if the xml does not contain the route-id then we fix this by adding the actual route id
318        // this may be needed if the route-id was auto-generated, as the intend is to update this route
319        // and not add a new route, adding a new route, use the MBean operation on ManagedCamelContext instead.
320        if (ObjectHelper.isEmpty(def.getId())) {
321            def.setId(getRouteId());
322        } else if (!def.getId().equals(getRouteId())) {
323            throw new IllegalArgumentException("Cannot update route from XML as routeIds does not match. routeId: "
324                    + getRouteId() + ", routeId from XML: " + def.getId());
325        }
326
327        LOG.debug("Updating route: {} from xml: {}", def.getId(), xml);
328
329        try {
330            // add will remove existing route first
331            context.addRouteDefinition(def);
332        } catch (Exception e) {
333            // log the error as warn as the management api may be invoked remotely over JMX which does not propagate such exception
334            String msg = "Error updating route: " + def.getId() + " from xml: " + xml + " due: " + e.getMessage();
335            LOG.warn(msg, e);
336            throw e;
337        }
338    }
339
340    public String dumpRouteStatsAsXml(boolean fullStats, boolean includeProcessors) throws Exception {
341        // in this logic we need to calculate the accumulated processing time for the processor in the route
342        // and hence why the logic is a bit more complicated to do this, as we need to calculate that from
343        // the bottom -> top of the route but this information is valuable for profiling routes
344        StringBuilder sb = new StringBuilder();
345
346        // need to calculate this value first, as we need that value for the route stat
347        Long processorAccumulatedTime = 0L;
348
349        // gather all the processors for this route, which requires JMX
350        if (includeProcessors) {
351            sb.append("  <processorStats>\n");
352            MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
353            if (server != null) {
354                // get all the processor mbeans and sort them accordingly to their index
355                String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
356                ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*");
357                Set<ObjectName> names = server.queryNames(query, null);
358                List<ManagedProcessorMBean> mps = new ArrayList<ManagedProcessorMBean>();
359                for (ObjectName on : names) {
360                    ManagedProcessorMBean processor = context.getManagementStrategy().getManagementAgent().newProxyClient(on, ManagedProcessorMBean.class);
361
362                    // the processor must belong to this route
363                    if (getRouteId().equals(processor.getRouteId())) {
364                        mps.add(processor);
365                    }
366                }
367                mps.sort(new OrderProcessorMBeans());
368
369                // walk the processors in reverse order, and calculate the accumulated total time
370                Map<String, Long> accumulatedTimes = new HashMap<String, Long>();
371                Collections.reverse(mps);
372                for (ManagedProcessorMBean processor : mps) {
373                    processorAccumulatedTime += processor.getTotalProcessingTime();
374                    accumulatedTimes.put(processor.getProcessorId(), processorAccumulatedTime);
375                }
376                // and reverse back again
377                Collections.reverse(mps);
378
379                // and now add the sorted list of processors to the xml output
380                for (ManagedProcessorMBean processor : mps) {
381                    sb.append("    <processorStat").append(String.format(" id=\"%s\" index=\"%s\" state=\"%s\"", processor.getProcessorId(), processor.getIndex(), processor.getState()));
382                    // do we have an accumulated time then append that
383                    Long accTime = accumulatedTimes.get(processor.getProcessorId());
384                    if (accTime != null) {
385                        sb.append(" accumulatedProcessingTime=\"").append(accTime).append("\"");
386                    }
387                    // use substring as we only want the attributes
388                    sb.append(" ").append(processor.dumpStatsAsXml(fullStats).substring(7)).append("\n");
389                }
390            }
391            sb.append("  </processorStats>\n");
392        }
393
394        // route self time is route total - processor accumulated total)
395        long routeSelfTime = getTotalProcessingTime() - processorAccumulatedTime;
396        if (routeSelfTime < 0) {
397            // ensure we don't calculate that as negative
398            routeSelfTime = 0;
399        }
400
401        StringBuilder answer = new StringBuilder();
402        answer.append("<routeStat").append(String.format(" id=\"%s\"", route.getId())).append(String.format(" state=\"%s\"", getState()));
403        // use substring as we only want the attributes
404        String stat = dumpStatsAsXml(fullStats);
405        answer.append(" exchangesInflight=\"").append(getInflightExchanges()).append("\"");
406        answer.append(" selfProcessingTime=\"").append(routeSelfTime).append("\"");
407        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
408        if (oldest == null) {
409            answer.append(" oldestInflightExchangeId=\"\"");
410            answer.append(" oldestInflightDuration=\"\"");
411        } else {
412            answer.append(" oldestInflightExchangeId=\"").append(oldest.getExchange().getExchangeId()).append("\"");
413            answer.append(" oldestInflightDuration=\"").append(oldest.getDuration()).append("\"");
414        }
415        answer.append(" ").append(stat.substring(7, stat.length() - 2)).append(">\n");
416
417        if (includeProcessors) {
418            answer.append(sb);
419        }
420
421        answer.append("</routeStat>");
422        return answer.toString();
423    }
424
425    public void reset(boolean includeProcessors) throws Exception {
426        reset();
427
428        // and now reset all processors for this route
429        if (includeProcessors) {
430            MBeanServer server = getContext().getManagementStrategy().getManagementAgent().getMBeanServer();
431            if (server != null) {
432                // get all the processor mbeans and sort them accordingly to their index
433                String prefix = getContext().getManagementStrategy().getManagementAgent().getIncludeHostName() ? "*/" : "";
434                ObjectName query = ObjectName.getInstance(jmxDomain + ":context=" + prefix + getContext().getManagementName() + ",type=processors,*");
435                QueryExp queryExp = Query.match(new AttributeValueExp("RouteId"), new StringValueExp(getRouteId()));
436                Set<ObjectName> names = server.queryNames(query, queryExp);
437                for (ObjectName name : names) {
438                    server.invoke(name, "reset", null, null);
439                }
440            }
441        }
442    }
443
444    public String createRouteStaticEndpointJson() {
445        return getContext().createRouteStaticEndpointJson(getRouteId());
446    }
447
448    @Override
449    public String createRouteStaticEndpointJson(boolean includeDynamic) {
450        return getContext().createRouteStaticEndpointJson(getRouteId(), includeDynamic);
451    }
452
453    @Override
454    public boolean equals(Object o) {
455        return this == o || (o != null && getClass() == o.getClass() && route.equals(((ManagedRoute) o).route));
456    }
457
458    @Override
459    public int hashCode() {
460        return route.hashCode();
461    }
462
463    private InflightRepository.InflightExchange getOldestInflightEntry() {
464        return getContext().getInflightRepository().oldest(getRouteId());
465    }
466
467    public Long getOldestInflightDuration() {
468        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
469        if (oldest == null) {
470            return null;
471        } else {
472            return oldest.getDuration();
473        }
474    }
475
476    public String getOldestInflightExchangeId() {
477        InflightRepository.InflightExchange oldest = getOldestInflightEntry();
478        if (oldest == null) {
479            return null;
480        } else {
481            return oldest.getExchange().getExchangeId();
482        }
483    }
484
485    @Override
486    public Boolean getHasRouteController() {
487        return route.getRouteContext().getRouteController() != null;
488    }
489
490    @Override
491    public RouteError getLastError() {
492        return route.getRouteContext().getLastError();
493    }
494
495    /**
496     * Used for sorting the processor mbeans accordingly to their index.
497     */
498    private static final class OrderProcessorMBeans implements Comparator<ManagedProcessorMBean> {
499
500        @Override
501        public int compare(ManagedProcessorMBean o1, ManagedProcessorMBean o2) {
502            return o1.getIndex().compareTo(o2.getIndex());
503        }
504    }
505}