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