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