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