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