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